diff --git a/node-graph/gcore/src/vector/algorithms/bezpath_algorithms.rs b/node-graph/gcore/src/vector/algorithms/bezpath_algorithms.rs new file mode 100644 index 00000000..406808eb --- /dev/null +++ b/node-graph/gcore/src/vector/algorithms/bezpath_algorithms.rs @@ -0,0 +1,108 @@ +/// Accuracy to find the position on [kurbo::Bezpath]. +const POSITION_ACCURACY: f64 = 1e-3; +/// Accuracy to find the length of the [kurbo::PathSeg]. +const PERIMETER_ACCURACY: f64 = 1e-3; + +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); + 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); + let segment = bezpath.get_seg(segment_index + 1).unwrap(); + match segment { + PathSeg::Line(line) => line.deriv().eval(t), + PathSeg::Quad(quad_bez) => quad_bez.deriv().eval(t), + PathSeg::Cubic(cubic_bez) => cubic_bez.deriv().eval(t), + } +} + +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)); + } + t_value_to_parametric(bezpath, BezPathTValue::GlobalParametric(t)) +} + +/// 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 { + let mut low_t = 0.; + let mut mid_t = 0.5; + let mut high_t = 1.; + + let total_length = path.perimeter(accuracy); + + if !total_length.is_finite() || total_length <= f64::EPSILON { + return 0.; + } + + 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_distance = current_length / total_length; + + if current_distance > distance { + high_t = mid_t; + } else { + low_t = mid_t; + } + mid_t = (high_t + low_t) / 2.; + } + + mid_t +} + +/// Converts from a bezpath (composed of multiple segments) to a point along a certain segment represented. +/// The returned tuple represents the segment index and the `t` value along that segment. +/// Both the input global `t` value and the output `t` value are in euclidean space, meaning there is a constant rate of change along the arc length. +fn global_euclidean_to_local_euclidean(bezpath: &kurbo::BezPath, global_t: f64, lengths: &[f64], total_length: f64) -> (usize, f64) { + let mut accumulator = 0.; + for (index, length) in lengths.iter().enumerate() { + let length_ratio = length / total_length; + if (index == 0 || accumulator <= global_t) && global_t <= accumulator + length_ratio { + return (index, ((global_t - accumulator) / length_ratio).clamp(0., 1.)); + } + accumulator += length_ratio; + } + (bezpath.segments().count() - 2, 1.) +} + +enum BezPathTValue { + GlobalEuclidean(f64), + GlobalParametric(f64), +} + +/// 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); + + match t { + BezPathTValue::GlobalEuclidean(t) => { + let lengths = bezpath.segments().map(|bezier| bezier.perimeter(PERIMETER_ACCURACY)).collect::>(); + let total_length: f64 = 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.); + } + + let scaled_t = global_t * segment_len as f64; + let segment_index = scaled_t.floor() as usize; + let t = scaled_t - segment_index as f64; + + (segment_index, t) + } + } +} diff --git a/node-graph/gcore/src/vector/algorithms/mod.rs b/node-graph/gcore/src/vector/algorithms/mod.rs index a03cf7c6..54fffd0d 100644 --- a/node-graph/gcore/src/vector/algorithms/mod.rs +++ b/node-graph/gcore/src/vector/algorithms/mod.rs @@ -1,3 +1,4 @@ +pub mod bezpath_algorithms; mod instance; mod merge_by_distance; pub mod offset_subpath; diff --git a/node-graph/gcore/src/vector/vector_nodes.rs b/node-graph/gcore/src/vector/vector_nodes.rs index 013d61f6..53e786fa 100644 --- a/node-graph/gcore/src/vector/vector_nodes.rs +++ b/node-graph/gcore/src/vector/vector_nodes.rs @@ -1,5 +1,6 @@ +use super::algorithms::bezpath_algorithms::{position_on_bezpath, tangent_on_bezpath}; use super::algorithms::offset_subpath::offset_subpath; -use super::misc::CentroidType; +use super::misc::{CentroidType, point_to_dvec2}; use super::style::{Fill, Gradient, GradientStops, Stroke}; use super::{PointId, SegmentDomain, SegmentId, StrokeId, VectorData, VectorDataTable}; use crate::instances::{Instance, InstanceMut, Instances}; @@ -14,6 +15,7 @@ use bezier_rs::{Join, ManipulatorGroup, Subpath, SubpathTValue, TValue}; use core::f64::consts::PI; use core::hash::{Hash, Hasher}; use glam::{DAffine2, DVec2}; +use kurbo::Affine; use rand::{Rng, SeedableRng}; use std::collections::hash_map::DefaultHasher; @@ -1304,16 +1306,17 @@ async fn position_on_path( let vector_data_transform = vector_data.transform(); let vector_data = vector_data.one_instance_ref().instance; - let subpaths_count = vector_data.stroke_bezier_paths().count() as f64; - let progress = progress.clamp(0., subpaths_count); - let progress = if reverse { subpaths_count - progress } else { progress }; - let index = if progress >= subpaths_count { (subpaths_count - 1.) as usize } else { progress as usize }; + let mut bezpaths = vector_data.stroke_bezpath_iter().collect::>(); + let bezpath_count = bezpaths.len() as f64; + let progress = progress.clamp(0., bezpath_count); + let progress = if reverse { bezpath_count - progress } else { progress }; + let index = if progress >= bezpath_count { (bezpath_count - 1.) as usize } else { progress as usize }; - vector_data.stroke_bezier_paths().nth(index).map_or(DVec2::ZERO, |mut subpath| { - subpath.apply_transform(vector_data_transform); + bezpaths.get_mut(index).map_or(DVec2::ZERO, |bezpath| { + let t = if progress == bezpath_count { 1. } else { progress.fract() }; + bezpath.apply_affine(Affine::new(vector_data_transform.to_cols_array())); - let t = if progress == subpaths_count { 1. } else { progress.fract() }; - subpath.evaluate(if euclidian { SubpathTValue::GlobalEuclidean(t) } else { SubpathTValue::GlobalParametric(t) }) + point_to_dvec2(position_on_bezpath(bezpath, t, euclidian)) }) } @@ -1336,19 +1339,20 @@ async fn tangent_on_path( let vector_data_transform = vector_data.transform(); let vector_data = vector_data.one_instance_ref().instance; - let subpaths_count = vector_data.stroke_bezier_paths().count() as f64; - let progress = progress.clamp(0., subpaths_count); - let progress = if reverse { subpaths_count - progress } else { progress }; - let index = if progress >= subpaths_count { (subpaths_count - 1.) as usize } else { progress as usize }; + let mut bezpaths = vector_data.stroke_bezpath_iter().collect::>(); + let bezpath_count = bezpaths.len() as f64; + let progress = progress.clamp(0., bezpath_count); + let progress = if reverse { bezpath_count - progress } else { progress }; + let index = if progress >= bezpath_count { (bezpath_count - 1.) as usize } else { progress as usize }; - vector_data.stroke_bezier_paths().nth(index).map_or(0., |mut subpath| { - subpath.apply_transform(vector_data_transform); + bezpaths.get_mut(index).map_or(0., |bezpath| { + let t = if progress == bezpath_count { 1. } else { progress.fract() }; + bezpath.apply_affine(Affine::new(vector_data_transform.to_cols_array())); - let t = if progress == subpaths_count { 1. } else { progress.fract() }; - let mut tangent = subpath.tangent(if euclidian { SubpathTValue::GlobalEuclidean(t) } else { SubpathTValue::GlobalParametric(t) }); + let mut tangent = point_to_dvec2(tangent_on_bezpath(bezpath, t, euclidian)); if tangent == DVec2::ZERO { let t = t + if t > 0.5 { -0.001 } else { 0.001 }; - tangent = subpath.tangent(if euclidian { SubpathTValue::GlobalEuclidean(t) } else { SubpathTValue::GlobalParametric(t) }); + tangent = point_to_dvec2(tangent_on_bezpath(bezpath, t, euclidian)); } if tangent == DVec2::ZERO { return 0.;