mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2025-07-08 00:05:00 +00:00
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:
parent
12896a2407
commit
d5cb380328
3 changed files with 131 additions and 18 deletions
108
node-graph/gcore/src/vector/algorithms/bezpath_algorithms.rs
Normal file
108
node-graph/gcore/src/vector/algorithms/bezpath_algorithms.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
pub mod bezpath_algorithms;
|
||||
mod instance;
|
||||
mod merge_by_distance;
|
||||
pub mod offset_subpath;
|
||||
|
|
|
@ -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.;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue