Refactor the 'Position on Path' and 'Tangent on Path' nodes to use the Kurbo API (#2611)

* rough refactor of Position on Path node

* refactor

* refactor 'Tangent on Path' node implementation to use kurbo API

* Code review

---------

Co-authored-by: indierusty <priyaayadav@gmail.com>
Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
Priyanshu 2025-04-30 10:21:10 +05:30 committed by GitHub
parent 12896a2407
commit d5cb380328
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 131 additions and 18 deletions

View file

@ -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::<Vec<f64>>();
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)
}
}
}

View file

@ -1,3 +1,4 @@
pub mod bezpath_algorithms;
mod instance;
mod merge_by_distance;
pub mod offset_subpath;

View file

@ -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::<Vec<kurbo::BezPath>>();
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::<Vec<kurbo::BezPath>>();
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.;