Skip to content

Commit d5cb380

Browse files
indierustyindierustyKeavon
authored
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 <[email protected]> Co-authored-by: Keavon Chambers <[email protected]>
1 parent 12896a2 commit d5cb380

File tree

3 files changed

+131
-18
lines changed

3 files changed

+131
-18
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
/// Accuracy to find the position on [kurbo::Bezpath].
2+
const POSITION_ACCURACY: f64 = 1e-3;
3+
/// Accuracy to find the length of the [kurbo::PathSeg].
4+
const PERIMETER_ACCURACY: f64 = 1e-3;
5+
6+
use kurbo::{BezPath, ParamCurve, ParamCurveDeriv, PathSeg, Point, Shape};
7+
8+
pub fn position_on_bezpath(bezpath: &BezPath, t: f64, euclidian: bool) -> Point {
9+
let (segment_index, t) = tvalue_to_parametric(bezpath, t, euclidian);
10+
bezpath.get_seg(segment_index + 1).unwrap().eval(t)
11+
}
12+
13+
pub fn tangent_on_bezpath(bezpath: &BezPath, t: f64, euclidian: bool) -> Point {
14+
let (segment_index, t) = tvalue_to_parametric(bezpath, t, euclidian);
15+
let segment = bezpath.get_seg(segment_index + 1).unwrap();
16+
match segment {
17+
PathSeg::Line(line) => line.deriv().eval(t),
18+
PathSeg::Quad(quad_bez) => quad_bez.deriv().eval(t),
19+
PathSeg::Cubic(cubic_bez) => cubic_bez.deriv().eval(t),
20+
}
21+
}
22+
23+
pub fn tvalue_to_parametric(bezpath: &BezPath, t: f64, euclidian: bool) -> (usize, f64) {
24+
if euclidian {
25+
let (segment_index, t) = t_value_to_parametric(bezpath, BezPathTValue::GlobalEuclidean(t));
26+
let segment = bezpath.get_seg(segment_index + 1).unwrap();
27+
return (segment_index, eval_pathseg_euclidian(segment, t, POSITION_ACCURACY));
28+
}
29+
t_value_to_parametric(bezpath, BezPathTValue::GlobalParametric(t))
30+
}
31+
32+
/// Finds the t value of point on the given path segment i.e fractional distance along the segment's total length.
33+
/// It uses a binary search to find the value `t` such that the ratio `length_upto_t / total_length` approximates the input `distance`.
34+
fn eval_pathseg_euclidian(path: kurbo::PathSeg, distance: f64, accuracy: f64) -> f64 {
35+
let mut low_t = 0.;
36+
let mut mid_t = 0.5;
37+
let mut high_t = 1.;
38+
39+
let total_length = path.perimeter(accuracy);
40+
41+
if !total_length.is_finite() || total_length <= f64::EPSILON {
42+
return 0.;
43+
}
44+
45+
let distance = distance.clamp(0., 1.);
46+
47+
while high_t - low_t > accuracy {
48+
let current_length = path.subsegment(0.0..mid_t).perimeter(accuracy);
49+
let current_distance = current_length / total_length;
50+
51+
if current_distance > distance {
52+
high_t = mid_t;
53+
} else {
54+
low_t = mid_t;
55+
}
56+
mid_t = (high_t + low_t) / 2.;
57+
}
58+
59+
mid_t
60+
}
61+
62+
/// Converts from a bezpath (composed of multiple segments) to a point along a certain segment represented.
63+
/// The returned tuple represents the segment index and the `t` value along that segment.
64+
/// 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.
65+
fn global_euclidean_to_local_euclidean(bezpath: &kurbo::BezPath, global_t: f64, lengths: &[f64], total_length: f64) -> (usize, f64) {
66+
let mut accumulator = 0.;
67+
for (index, length) in lengths.iter().enumerate() {
68+
let length_ratio = length / total_length;
69+
if (index == 0 || accumulator <= global_t) && global_t <= accumulator + length_ratio {
70+
return (index, ((global_t - accumulator) / length_ratio).clamp(0., 1.));
71+
}
72+
accumulator += length_ratio;
73+
}
74+
(bezpath.segments().count() - 2, 1.)
75+
}
76+
77+
enum BezPathTValue {
78+
GlobalEuclidean(f64),
79+
GlobalParametric(f64),
80+
}
81+
82+
/// Convert a [BezPathTValue] to a parametric `(segment_index, t)` tuple.
83+
/// - Asserts that `t` values contained within the `SubpathTValue` argument lie in the range [0, 1].
84+
fn t_value_to_parametric(bezpath: &kurbo::BezPath, t: BezPathTValue) -> (usize, f64) {
85+
let segment_len = bezpath.segments().count();
86+
assert!(segment_len >= 1);
87+
88+
match t {
89+
BezPathTValue::GlobalEuclidean(t) => {
90+
let lengths = bezpath.segments().map(|bezier| bezier.perimeter(PERIMETER_ACCURACY)).collect::<Vec<f64>>();
91+
let total_length: f64 = lengths.iter().sum();
92+
global_euclidean_to_local_euclidean(bezpath, t, lengths.as_slice(), total_length)
93+
}
94+
BezPathTValue::GlobalParametric(global_t) => {
95+
assert!((0.0..=1.).contains(&global_t));
96+
97+
if global_t == 1. {
98+
return (segment_len - 1, 1.);
99+
}
100+
101+
let scaled_t = global_t * segment_len as f64;
102+
let segment_index = scaled_t.floor() as usize;
103+
let t = scaled_t - segment_index as f64;
104+
105+
(segment_index, t)
106+
}
107+
}
108+
}
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
pub mod bezpath_algorithms;
12
mod instance;
23
mod merge_by_distance;
34
pub mod offset_subpath;

node-graph/gcore/src/vector/vector_nodes.rs

+22-18
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1+
use super::algorithms::bezpath_algorithms::{position_on_bezpath, tangent_on_bezpath};
12
use super::algorithms::offset_subpath::offset_subpath;
2-
use super::misc::CentroidType;
3+
use super::misc::{CentroidType, point_to_dvec2};
34
use super::style::{Fill, Gradient, GradientStops, Stroke};
45
use super::{PointId, SegmentDomain, SegmentId, StrokeId, VectorData, VectorDataTable};
56
use crate::instances::{Instance, InstanceMut, Instances};
@@ -14,6 +15,7 @@ use bezier_rs::{Join, ManipulatorGroup, Subpath, SubpathTValue, TValue};
1415
use core::f64::consts::PI;
1516
use core::hash::{Hash, Hasher};
1617
use glam::{DAffine2, DVec2};
18+
use kurbo::Affine;
1719
use rand::{Rng, SeedableRng};
1820
use std::collections::hash_map::DefaultHasher;
1921

@@ -1304,16 +1306,17 @@ async fn position_on_path(
13041306
let vector_data_transform = vector_data.transform();
13051307
let vector_data = vector_data.one_instance_ref().instance;
13061308

1307-
let subpaths_count = vector_data.stroke_bezier_paths().count() as f64;
1308-
let progress = progress.clamp(0., subpaths_count);
1309-
let progress = if reverse { subpaths_count - progress } else { progress };
1310-
let index = if progress >= subpaths_count { (subpaths_count - 1.) as usize } else { progress as usize };
1309+
let mut bezpaths = vector_data.stroke_bezpath_iter().collect::<Vec<kurbo::BezPath>>();
1310+
let bezpath_count = bezpaths.len() as f64;
1311+
let progress = progress.clamp(0., bezpath_count);
1312+
let progress = if reverse { bezpath_count - progress } else { progress };
1313+
let index = if progress >= bezpath_count { (bezpath_count - 1.) as usize } else { progress as usize };
13111314

1312-
vector_data.stroke_bezier_paths().nth(index).map_or(DVec2::ZERO, |mut subpath| {
1313-
subpath.apply_transform(vector_data_transform);
1315+
bezpaths.get_mut(index).map_or(DVec2::ZERO, |bezpath| {
1316+
let t = if progress == bezpath_count { 1. } else { progress.fract() };
1317+
bezpath.apply_affine(Affine::new(vector_data_transform.to_cols_array()));
13141318

1315-
let t = if progress == subpaths_count { 1. } else { progress.fract() };
1316-
subpath.evaluate(if euclidian { SubpathTValue::GlobalEuclidean(t) } else { SubpathTValue::GlobalParametric(t) })
1319+
point_to_dvec2(position_on_bezpath(bezpath, t, euclidian))
13171320
})
13181321
}
13191322

@@ -1336,19 +1339,20 @@ async fn tangent_on_path(
13361339
let vector_data_transform = vector_data.transform();
13371340
let vector_data = vector_data.one_instance_ref().instance;
13381341

1339-
let subpaths_count = vector_data.stroke_bezier_paths().count() as f64;
1340-
let progress = progress.clamp(0., subpaths_count);
1341-
let progress = if reverse { subpaths_count - progress } else { progress };
1342-
let index = if progress >= subpaths_count { (subpaths_count - 1.) as usize } else { progress as usize };
1342+
let mut bezpaths = vector_data.stroke_bezpath_iter().collect::<Vec<kurbo::BezPath>>();
1343+
let bezpath_count = bezpaths.len() as f64;
1344+
let progress = progress.clamp(0., bezpath_count);
1345+
let progress = if reverse { bezpath_count - progress } else { progress };
1346+
let index = if progress >= bezpath_count { (bezpath_count - 1.) as usize } else { progress as usize };
13431347

1344-
vector_data.stroke_bezier_paths().nth(index).map_or(0., |mut subpath| {
1345-
subpath.apply_transform(vector_data_transform);
1348+
bezpaths.get_mut(index).map_or(0., |bezpath| {
1349+
let t = if progress == bezpath_count { 1. } else { progress.fract() };
1350+
bezpath.apply_affine(Affine::new(vector_data_transform.to_cols_array()));
13461351

1347-
let t = if progress == subpaths_count { 1. } else { progress.fract() };
1348-
let mut tangent = subpath.tangent(if euclidian { SubpathTValue::GlobalEuclidean(t) } else { SubpathTValue::GlobalParametric(t) });
1352+
let mut tangent = point_to_dvec2(tangent_on_bezpath(bezpath, t, euclidian));
13491353
if tangent == DVec2::ZERO {
13501354
let t = t + if t > 0.5 { -0.001 } else { 0.001 };
1351-
tangent = subpath.tangent(if euclidian { SubpathTValue::GlobalEuclidean(t) } else { SubpathTValue::GlobalParametric(t) });
1355+
tangent = point_to_dvec2(tangent_on_bezpath(bezpath, t, euclidian));
13521356
}
13531357
if tangent == DVec2::ZERO {
13541358
return 0.;

0 commit comments

Comments
 (0)