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:
Rob Nadal
2023-02-17 15:33:52 -05:00
committed by Keavon Chambers
co-authored by Hannah Li Keavon Chambers
parent 344f243432
commit 9a52cae9b9
10 changed files with 334 additions and 247 deletions
+112 -1
View File
@@ -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.));
}
}