diff --git a/node-graph/gcore/src/vector/algorithms/bezpath_algorithms.rs b/node-graph/gcore/src/vector/algorithms/bezpath_algorithms.rs index 406808eb..85a0401a 100644 --- a/node-graph/gcore/src/vector/algorithms/bezpath_algorithms.rs +++ b/node-graph/gcore/src/vector/algorithms/bezpath_algorithms.rs @@ -1,17 +1,17 @@ /// Accuracy to find the position on [kurbo::Bezpath]. -const POSITION_ACCURACY: f64 = 1e-3; +const POSITION_ACCURACY: f64 = 1e-5; /// Accuracy to find the length of the [kurbo::PathSeg]. -const PERIMETER_ACCURACY: f64 = 1e-3; +pub const PERIMETER_ACCURACY: f64 = 1e-5; use kurbo::{BezPath, ParamCurve, ParamCurveDeriv, PathSeg, Point, Shape}; -pub fn position_on_bezpath(bezpath: &BezPath, t: f64, euclidian: bool) -> Point { - let (segment_index, t) = tvalue_to_parametric(bezpath, t, euclidian); +pub fn position_on_bezpath(bezpath: &BezPath, t: f64, euclidian: bool, segments_length: Option<&[f64]>) -> Point { + let (segment_index, t) = t_value_to_parametric(bezpath, t, euclidian, segments_length); bezpath.get_seg(segment_index + 1).unwrap().eval(t) } -pub fn tangent_on_bezpath(bezpath: &BezPath, t: f64, euclidian: bool) -> Point { - let (segment_index, t) = tvalue_to_parametric(bezpath, t, euclidian); +pub fn tangent_on_bezpath(bezpath: &BezPath, t: f64, euclidian: bool, segments_length: Option<&[f64]>) -> Point { + let (segment_index, t) = t_value_to_parametric(bezpath, t, euclidian, segments_length); let segment = bezpath.get_seg(segment_index + 1).unwrap(); match segment { PathSeg::Line(line) => line.deriv().eval(t), @@ -20,23 +20,92 @@ pub fn tangent_on_bezpath(bezpath: &BezPath, t: f64, euclidian: bool) -> Point { } } -pub fn tvalue_to_parametric(bezpath: &BezPath, t: f64, euclidian: bool) -> (usize, f64) { - if euclidian { - let (segment_index, t) = t_value_to_parametric(bezpath, BezPathTValue::GlobalEuclidean(t)); - let segment = bezpath.get_seg(segment_index + 1).unwrap(); - return (segment_index, eval_pathseg_euclidian(segment, t, POSITION_ACCURACY)); +pub fn sample_points_on_bezpath(bezpath: BezPath, spacing: f64, start_offset: f64, stop_offset: f64, adaptive_spacing: bool, segments_length: &[f64]) -> Option { + let mut sample_bezpath = BezPath::new(); + + // Calculate the total length of the collected segments. + let total_length: f64 = segments_length.iter().sum(); + + // Adjust the usable length by subtracting start and stop offsets. + let mut used_length = total_length - start_offset - stop_offset; + + if used_length <= 0. { + return None; } - t_value_to_parametric(bezpath, BezPathTValue::GlobalParametric(t)) + + // Determine the number of points to generate along the path. + let sample_count = if adaptive_spacing { + // Calculate point count to evenly distribute points while covering the entire path. + // With adaptive spacing, we widen or narrow the points as necessary to ensure the last point is always at the end of the path. + (used_length / spacing).round() + } else { + // Calculate point count based on exact spacing, which may not cover the entire path. + + // Without adaptive spacing, we just evenly space the points at the exact specified spacing, usually falling short before the end of the path. + let count = (used_length / spacing + f64::EPSILON).floor(); + used_length -= used_length % spacing; + count + }; + + // Skip if there are no points to generate. + if sample_count < 1. { + return None; + } + + // Generate points along the path based on calculated intervals. + let mut length_up_to_previous_segment = 0.; + let mut next_segment_index = 0; + + for count in 0..=sample_count as usize { + let fraction = count as f64 / sample_count; + let length_up_to_next_sample_point = fraction * used_length + start_offset; + let mut next_length = length_up_to_next_sample_point - length_up_to_previous_segment; + let mut next_segment_length = segments_length[next_segment_index]; + + // Keep moving to the next segment while the length up to the next sample point is less or equals to the length up to the segment. + while next_length > next_segment_length { + if next_segment_index == segments_length.len() - 1 { + break; + } + length_up_to_previous_segment += next_segment_length; + next_length = length_up_to_next_sample_point - length_up_to_previous_segment; + next_segment_index += 1; + next_segment_length = segments_length[next_segment_index]; + } + + let t = (next_length / next_segment_length).clamp(0., 1.); + + let segment = bezpath.get_seg(next_segment_index + 1).unwrap(); + let t = eval_pathseg_euclidean(segment, t, POSITION_ACCURACY); + let point = segment.eval(t); + + if sample_bezpath.elements().is_empty() { + sample_bezpath.move_to(point) + } else { + sample_bezpath.line_to(point) + } + } + + Some(sample_bezpath) +} + +pub fn t_value_to_parametric(bezpath: &BezPath, t: f64, euclidian: bool, segments_length: Option<&[f64]>) -> (usize, f64) { + if euclidian { + let (segment_index, t) = bezpath_t_value_to_parametric(bezpath, BezPathTValue::GlobalEuclidean(t), segments_length); + let segment = bezpath.get_seg(segment_index + 1).unwrap(); + return (segment_index, eval_pathseg_euclidean(segment, t, POSITION_ACCURACY)); + } + bezpath_t_value_to_parametric(bezpath, BezPathTValue::GlobalParametric(t), segments_length) } /// Finds the t value of point on the given path segment i.e fractional distance along the segment's total length. -/// It uses a binary search to find the value `t` such that the ratio `length_upto_t / total_length` approximates the input `distance`. -fn eval_pathseg_euclidian(path: kurbo::PathSeg, distance: f64, accuracy: f64) -> f64 { +/// It uses a binary search to find the value `t` such that the ratio `length_up_to_t / total_length` approximates the input `distance`. +pub fn eval_pathseg_euclidean(path_segment: kurbo::PathSeg, distance: f64, accuracy: f64) -> f64 { let mut low_t = 0.; let mut mid_t = 0.5; let mut high_t = 1.; - let total_length = path.perimeter(accuracy); + let total_length = path_segment.perimeter(accuracy); if !total_length.is_finite() || total_length <= f64::EPSILON { return 0.; @@ -45,7 +114,7 @@ fn eval_pathseg_euclidian(path: kurbo::PathSeg, distance: f64, accuracy: f64) -> let distance = distance.clamp(0., 1.); while high_t - low_t > accuracy { - let current_length = path.subsegment(0.0..mid_t).perimeter(accuracy); + let current_length = path_segment.subsegment(0.0..mid_t).perimeter(accuracy); let current_distance = current_length / total_length; if current_distance > distance { @@ -71,7 +140,7 @@ fn global_euclidean_to_local_euclidean(bezpath: &kurbo::BezPath, global_t: f64, } accumulator += length_ratio; } - (bezpath.segments().count() - 2, 1.) + (bezpath.segments().count() - 1, 1.) } enum BezPathTValue { @@ -81,24 +150,28 @@ enum BezPathTValue { /// Convert a [BezPathTValue] to a parametric `(segment_index, t)` tuple. /// - Asserts that `t` values contained within the `SubpathTValue` argument lie in the range [0, 1]. -fn t_value_to_parametric(bezpath: &kurbo::BezPath, t: BezPathTValue) -> (usize, f64) { - let segment_len = bezpath.segments().count(); - assert!(segment_len >= 1); +fn bezpath_t_value_to_parametric(bezpath: &kurbo::BezPath, t: BezPathTValue, segments_length: Option<&[f64]>) -> (usize, f64) { + let segment_count = bezpath.segments().count(); + assert!(segment_count >= 1); match t { BezPathTValue::GlobalEuclidean(t) => { - let lengths = bezpath.segments().map(|bezier| bezier.perimeter(PERIMETER_ACCURACY)).collect::>(); - let total_length: f64 = lengths.iter().sum(); + let lengths = segments_length + .map(|segments_length| segments_length.to_vec()) + .unwrap_or(bezpath.segments().map(|segment| segment.perimeter(PERIMETER_ACCURACY)).collect()); + + let total_length = lengths.iter().sum(); + global_euclidean_to_local_euclidean(bezpath, t, lengths.as_slice(), total_length) } BezPathTValue::GlobalParametric(global_t) => { assert!((0.0..=1.).contains(&global_t)); if global_t == 1. { - return (segment_len - 1, 1.); + return (segment_count - 1, 1.); } - let scaled_t = global_t * segment_len as f64; + let scaled_t = global_t * segment_count as f64; let segment_index = scaled_t.floor() as usize; let t = scaled_t - segment_index as f64; diff --git a/node-graph/gcore/src/vector/vector_nodes.rs b/node-graph/gcore/src/vector/vector_nodes.rs index 50cd60b8..85082bd1 100644 --- a/node-graph/gcore/src/vector/vector_nodes.rs +++ b/node-graph/gcore/src/vector/vector_nodes.rs @@ -1,4 +1,4 @@ -use super::algorithms::bezpath_algorithms::{position_on_bezpath, tangent_on_bezpath}; +use super::algorithms::bezpath_algorithms::{PERIMETER_ACCURACY, position_on_bezpath, sample_points_on_bezpath, tangent_on_bezpath}; use super::algorithms::offset_subpath::offset_subpath; use super::misc::{CentroidType, point_to_dvec2}; use super::style::{Fill, Gradient, GradientStops, Stroke}; @@ -11,11 +11,11 @@ use crate::transform::{Footprint, ReferencePoint, Transform, TransformMut}; use crate::vector::PointDomain; use crate::vector::style::{LineCap, LineJoin}; use crate::{CloneVarArgs, Color, Context, Ctx, ExtractAll, GraphicElement, GraphicGroupTable, OwnedContextImpl}; -use bezier_rs::{Join, ManipulatorGroup, Subpath, SubpathTValue, TValue}; +use bezier_rs::{Join, ManipulatorGroup, Subpath, SubpathTValue}; use core::f64::consts::PI; use core::hash::{Hash, Hasher}; use glam::{DAffine2, DVec2}; -use kurbo::Affine; +use kurbo::{Affine, Shape}; use rand::{Rng, SeedableRng}; use std::collections::hash_map::DefaultHasher; @@ -1147,144 +1147,43 @@ async fn sample_points(_: impl Ctx, vector_data: VectorDataTable, spacing: f64, let spacing = spacing.max(0.01); let vector_data_transform = vector_data.transform(); - let vector_data = vector_data.one_instance_ref().instance; - // Create an iterator over the bezier segments with enumeration and peeking capability. - let mut bezier = vector_data.segment_bezier_iter().enumerate().peekable(); + // Using `stroke_bezpath_iter` so that the `subpath_segment_lengths` is aligned to the segments of each bezpath. + // So we can index into `subpath_segment_lengths` to get the length of the segments. + // NOTE: `subpath_segment_lengths` has precalulated lengths with transformation applied. + let bezpaths = vector_data.one_instance_ref().instance.stroke_bezpath_iter(); // Initialize the result VectorData with the same transformation as the input. let mut result = VectorDataTable::default(); *result.transform_mut() = vector_data_transform; - // Iterate over each segment in the bezier iterator. - while let Some((index, (segment_id, _, start_point_index, mut last_end))) = bezier.next() { - // Record the start point index of the subpath. - let subpath_start_point_index = start_point_index; + // Keeps track of the index of the first segment of the next bezpath in order to get lengths of all segments. + let mut next_segment_index = 0; - // Collect connected segments that form a continuous path. - let mut lengths = vec![(segment_id, subpath_segment_lengths.get(index).copied().unwrap_or_default())]; + for mut bezpath in bezpaths { + // Apply the tranformation to the current bezpath to calculate points after transformation. + bezpath.apply_affine(Affine::new(vector_data_transform.to_cols_array())); - // Continue collecting segments as long as they are connected end-to-start. - while let Some(&seg) = bezier.peek() { - let (_, (_, _, ref start, _)) = seg; - if *start == last_end { - // Consume the next element since it continues the path. - let (index, (next_segment_id, _, _, end)) = bezier.next().unwrap(); - last_end = end; - lengths.push((next_segment_id, subpath_segment_lengths.get(index).copied().unwrap_or_default())); - } else { - // The next segment does not continue the path. - break; - } - } + let segment_count = bezpath.segments().count(); - // Determine if the subpath is closed. - let subpath_is_closed = last_end == subpath_start_point_index; + // For the current bezpath we get its segment's length by calculating the start index and end index. + let current_bezpath_segments_length = &subpath_segment_lengths[next_segment_index..next_segment_index + segment_count]; - // Calculate the total length of the collected segments. - let total_length: f64 = lengths.iter().map(|(_, len)| *len).sum(); + // Increment the segment index by the number of segments in the current bezpath to calculate the next bezpath segment's length. + next_segment_index += segment_count; - // Adjust the usable length by subtracting start and stop offsets. - let mut used_length = total_length - start_offset - stop_offset; - if used_length <= 0. { + let Some(mut sample_bezpath) = sample_points_on_bezpath(bezpath, spacing, start_offset, stop_offset, adaptive_spacing, current_bezpath_segments_length) else { continue; - } - - // Determine the number of points to generate along the path. - let count = if adaptive_spacing { - // Calculate point count to evenly distribute points while covering the entire path. - // With adaptive spacing, we widen or narrow the points as necessary to ensure the last point is always at the end of the path. - (used_length / spacing).round() - } else { - // Calculate point count based on exact spacing, which may not cover the entire path. - - // Without adaptive spacing, we just evenly space the points at the exact specified spacing, usually falling short before the end of the path. - let c = (used_length / spacing + f64::EPSILON).floor(); - used_length -= used_length % spacing; - c }; - // Skip if there are no points to generate. - if count < 1. { - continue; - } + // Reverse the transformation applied to the bezpath as the `result` already has the transformation set. + sample_bezpath.apply_affine(Affine::new(vector_data_transform.to_cols_array()).inverse()); - // Initialize a vector to store indices of generated points. - let mut point_indices = Vec::new(); - - // Generate points along the path based on calculated intervals. - let max_c = if subpath_is_closed { count as usize - 1 } else { count as usize }; - for c in 0..=max_c { - let fraction = c as f64 / count; - let total_distance = fraction * used_length + start_offset; - - // Find the segment corresponding to the current total_distance. - let (mut current_segment_id, mut length) = lengths[0]; - let mut total_length_before = 0.; - for &(next_segment_id, next_length) in lengths.iter().skip(1) { - if total_length_before + length > total_distance { - break; - } - - total_length_before += length; - current_segment_id = next_segment_id; - length = next_length; - } - - // Retrieve the segment and apply transformation. - let Some(segment) = vector_data.segment_from_id(current_segment_id) else { continue }; - let segment = segment.apply_transformation(|point| vector_data_transform.transform_point2(point)); - - // Calculate the position on the segment. - let parametric_t = segment.euclidean_to_parametric_with_total_length((total_distance - total_length_before) / length, 0.001, length); - let point = segment.evaluate(TValue::Parametric(parametric_t)); - - // Generate a new PointId and add the point to result.point_domain. - let point_id = PointId::generate(); - result.one_instance_mut().instance.point_domain.push(point_id, vector_data_transform.inverse().transform_point2(point)); - - // Store the index of the point. - let point_index = result.one_instance_mut().instance.point_domain.ids().len() - 1; - point_indices.push(point_index); - } - - // After generating points, create segments between consecutive points. - for window in point_indices.windows(2) { - if let [start_index, end_index] = *window { - // Generate a new SegmentId. - let segment_id = SegmentId::generate(); - - // Use BezierHandles::Linear for linear segments. - let handles = bezier_rs::BezierHandles::Linear; - - // Generate a new StrokeId. - let stroke_id = StrokeId::generate(); - - // Add the segment to result.segment_domain. - result.one_instance_mut().instance.segment_domain.push(segment_id, start_index, end_index, handles, stroke_id); - } - } - - // If the subpath is closed, add a closing segment connecting the last point to the first point. - if subpath_is_closed { - if let (Some(&first_index), Some(&last_index)) = (point_indices.first(), point_indices.last()) { - // Generate a new SegmentId. - let segment_id = SegmentId::generate(); - - // Use BezierHandles::Linear for linear segments. - let handles = bezier_rs::BezierHandles::Linear; - - // Generate a new StrokeId. - let stroke_id = StrokeId::generate(); - - // Add the closing segment to result.segment_domain. - result.one_instance_mut().instance.segment_domain.push(segment_id, last_index, first_index, handles, stroke_id); - } - } + // Append the bezpath (subpath) that connects generated points by lines. + result.one_instance_mut().instance.append_bezpath(sample_bezpath); } - // Transfer the style from the input vector data to the result. - result.one_instance_mut().instance.style = vector_data.style.clone(); + result.one_instance_mut().instance.style = vector_data.one_instance_ref().instance.style.clone(); result.one_instance_mut().instance.style.set_stroke_transform(vector_data_transform); // Return the resulting vector data with newly generated points and segments. @@ -1320,7 +1219,7 @@ async fn position_on_path( let t = if progress == bezpath_count { 1. } else { progress.fract() }; bezpath.apply_affine(Affine::new(vector_data_transform.to_cols_array())); - point_to_dvec2(position_on_bezpath(bezpath, t, euclidian)) + point_to_dvec2(position_on_bezpath(bezpath, t, euclidian, None)) }) } @@ -1353,10 +1252,10 @@ async fn tangent_on_path( let t = if progress == bezpath_count { 1. } else { progress.fract() }; bezpath.apply_affine(Affine::new(vector_data_transform.to_cols_array())); - let mut tangent = point_to_dvec2(tangent_on_bezpath(bezpath, t, euclidian)); + let mut tangent = point_to_dvec2(tangent_on_bezpath(bezpath, t, euclidian, None)); if tangent == DVec2::ZERO { let t = t + if t > 0.5 { -0.001 } else { 0.001 }; - tangent = point_to_dvec2(tangent_on_bezpath(bezpath, t, euclidian)); + tangent = point_to_dvec2(tangent_on_bezpath(bezpath, t, euclidian, None)); } if tangent == DVec2::ZERO { return 0.; @@ -1430,8 +1329,11 @@ async fn subpath_segment_lengths(_: impl Ctx, vector_data: VectorDataTable) -> V let vector_data = vector_data.one_instance_ref().instance; vector_data - .segment_bezier_iter() - .map(|(_id, bezier, _, _)| bezier.apply_transformation(|point| vector_data_transform.transform_point2(point)).length(None)) + .stroke_bezpath_iter() + .flat_map(|mut bezpath| { + bezpath.apply_affine(Affine::new(vector_data_transform.to_cols_array())); + bezpath.segments().map(|segment| segment.perimeter(PERIMETER_ACCURACY)).collect::>() + }) .collect() }