Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Apr 30, 2025
Merged
Show file tree
Hide file tree
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: 108 additions & 0 deletions node-graph/gcore/src/vector/algorithms/bezpath_algorithms.rs
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
1 change: 1 addition & 0 deletions node-graph/gcore/src/vector/algorithms/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod bezpath_algorithms;
mod instance;
mod merge_by_distance;
pub mod offset_subpath;
40 changes: 22 additions & 18 deletions node-graph/gcore/src/vector/vector_nodes.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand All @@ -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;

Expand Down Expand Up @@ -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))
})
}

Expand All @@ -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.;
Expand Down
Loading