mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-10-05 22:38:11 +08:00
Bezier-rs: Add SubpathTValue and euclidean parameterization for subpaths (#1027)
* Added SubpathTValue and euclidean parameterization for subpaths * Small fix * Added bounds checking to get_segment * Code review * code review nit for clarity --------- Co-authored-by: Hannah Li <hannahli2010@gmail.com> Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
committed by
Keavon Chambers
co-authored by
Hannah Li
Keavon Chambers
parent
344f243432
commit
9a52cae9b9
@@ -1,5 +1,7 @@
|
||||
use super::*;
|
||||
use crate::{ProjectionOptions, TValue};
|
||||
use crate::consts::DEFAULT_EUCLIDEAN_ERROR_BOUND;
|
||||
use crate::utils::{SubpathTValue, TValue};
|
||||
use crate::ProjectionOptions;
|
||||
use glam::DVec2;
|
||||
|
||||
/// Functionality relating to looking up properties of the `Subpath` or points along the `Subpath`.
|
||||
@@ -10,6 +12,70 @@ impl Subpath {
|
||||
self.iter().fold(0., |accumulator, bezier| accumulator + bezier.length(num_subdivisions))
|
||||
}
|
||||
|
||||
fn global_euclidean_to_local_euclidean(&self, global_t: f64) -> (usize, f64) {
|
||||
let lengths = self.iter().map(|bezier| bezier.length(None)).collect::<Vec<f64>>();
|
||||
let total_length: f64 = lengths.iter().sum();
|
||||
|
||||
let mut accumulator = 0.;
|
||||
for (index, length) in lengths.iter().enumerate() {
|
||||
let length_ratio = length / total_length;
|
||||
if accumulator <= global_t && global_t <= accumulator + length_ratio {
|
||||
return (index, (global_t - accumulator) / length_ratio);
|
||||
}
|
||||
accumulator += length_ratio;
|
||||
}
|
||||
(0, 0.)
|
||||
}
|
||||
|
||||
/// Convert a [SubpathTValue] to a parametric `(segment_index, t)` tuple.
|
||||
/// - Asserts that `t` values contained within the `SubpathTValue` argument lie in the range [0, 1].
|
||||
/// - If the argument is a variant containing a `segment_index`, asserts that the index references a valid segment on the curve.
|
||||
pub(crate) fn t_value_to_parametric(&self, t: SubpathTValue) -> (usize, f64) {
|
||||
assert!(self.len_segments() >= 1);
|
||||
|
||||
match t {
|
||||
SubpathTValue::Parametric { segment_index, t } => {
|
||||
assert!((0.0..=1.).contains(&t));
|
||||
assert!((0..self.len_segments() - 1).contains(&segment_index));
|
||||
(segment_index, t)
|
||||
}
|
||||
SubpathTValue::GlobalParametric(global_t) => {
|
||||
assert!((0.0..=1.).contains(&global_t));
|
||||
|
||||
if global_t == 1. {
|
||||
return (self.len_segments() - 1, 1.);
|
||||
}
|
||||
|
||||
let scaled_t = global_t * self.len_segments() as f64;
|
||||
let segment_index = scaled_t.floor() as usize;
|
||||
let t = scaled_t - segment_index as f64;
|
||||
|
||||
(segment_index, t)
|
||||
}
|
||||
SubpathTValue::Euclidean { segment_index, t } => {
|
||||
assert!((0.0..=1.).contains(&t));
|
||||
assert!((0..self.len_segments()).contains(&segment_index));
|
||||
(segment_index, self.get_segment(segment_index).unwrap().euclidean_to_parametric(t, DEFAULT_EUCLIDEAN_ERROR_BOUND))
|
||||
}
|
||||
SubpathTValue::GlobalEuclidean(t) => {
|
||||
let (segment_index, segment_t) = self.global_euclidean_to_local_euclidean(t);
|
||||
(
|
||||
segment_index,
|
||||
self.get_segment(segment_index).unwrap().euclidean_to_parametric(segment_t, DEFAULT_EUCLIDEAN_ERROR_BOUND),
|
||||
)
|
||||
}
|
||||
SubpathTValue::EuclideanWithinError { segment_index, t, error } => {
|
||||
assert!((0.0..=1.).contains(&t));
|
||||
assert!((0..self.len_segments()).contains(&segment_index));
|
||||
(segment_index, self.get_segment(segment_index).unwrap().euclidean_to_parametric(t, error))
|
||||
}
|
||||
SubpathTValue::GlobalEuclideanWithinError { t, error } => {
|
||||
let (segment_index, segment_t) = self.global_euclidean_to_local_euclidean(t);
|
||||
(segment_index, self.get_segment(segment_index).unwrap().euclidean_to_parametric(segment_t, error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the segment index and `t` value that corresponds to the closest point on the curve to the provided point.
|
||||
/// Uses a searching algorithm akin to binary search that can be customized using the [ProjectionOptions] structure.
|
||||
pub fn project(&self, point: DVec2, options: ProjectionOptions) -> Option<(usize, f64)> {
|
||||
@@ -34,6 +100,9 @@ impl Subpath {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::consts::MAX_ABSOLUTE_DIFFERENCE;
|
||||
use crate::utils::f64_compare;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
@@ -113,4 +182,46 @@ mod tests {
|
||||
subpath.closed = true;
|
||||
assert_eq!(subpath.length(None), linear_bezier.length(None) + quadratic_bezier.length(None) + cubic_bezier.length(None));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn t_value_to_parametric_global_parametric_open_subpath() {
|
||||
let mock_manipulator_group = ManipulatorGroup {
|
||||
anchor: DVec2::new(0., 0.),
|
||||
in_handle: None,
|
||||
out_handle: None,
|
||||
};
|
||||
let open_subpath = Subpath {
|
||||
manipulator_groups: vec![mock_manipulator_group; 5],
|
||||
closed: false,
|
||||
};
|
||||
|
||||
let (segment_index, t) = open_subpath.t_value_to_parametric(SubpathTValue::GlobalParametric(0.7));
|
||||
assert_eq!(segment_index, 2);
|
||||
assert!(f64_compare(t, 0.8, MAX_ABSOLUTE_DIFFERENCE));
|
||||
|
||||
// The start and end points of an open subpath are NOT equivalent
|
||||
assert_eq!(open_subpath.t_value_to_parametric(SubpathTValue::GlobalParametric(0.)), (0, 0.));
|
||||
assert_eq!(open_subpath.t_value_to_parametric(SubpathTValue::GlobalParametric(1.)), (3, 1.));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn t_value_to_parametric_global_parametric_closed_subpath() {
|
||||
let mock_manipulator_group = ManipulatorGroup {
|
||||
anchor: DVec2::new(0., 0.),
|
||||
in_handle: None,
|
||||
out_handle: None,
|
||||
};
|
||||
let closed_subpath = Subpath {
|
||||
manipulator_groups: vec![mock_manipulator_group; 5],
|
||||
closed: true,
|
||||
};
|
||||
|
||||
let (segment_index, t) = closed_subpath.t_value_to_parametric(SubpathTValue::GlobalParametric(0.7));
|
||||
assert_eq!(segment_index, 3);
|
||||
assert!(f64_compare(t, 0.5, MAX_ABSOLUTE_DIFFERENCE));
|
||||
|
||||
// The start and end points of a closed subpath are equivalent
|
||||
assert_eq!(closed_subpath.t_value_to_parametric(SubpathTValue::GlobalParametric(0.)), (0, 0.));
|
||||
assert_eq!(closed_subpath.t_value_to_parametric(SubpathTValue::GlobalParametric(1.)), (4, 1.));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user