diff --git a/libraries/bezier-rs/src/lib.rs b/libraries/bezier-rs/src/lib.rs index e12545c5..bc5f12a9 100644 --- a/libraries/bezier-rs/src/lib.rs +++ b/libraries/bezier-rs/src/lib.rs @@ -7,4 +7,4 @@ mod utils; pub use bezier::*; pub use subpath::*; -pub use utils::TValue; +pub use utils::{SubpathTValue, TValue}; diff --git a/libraries/bezier-rs/src/subpath/core.rs b/libraries/bezier-rs/src/subpath/core.rs index 1938e6e0..f2de0459 100644 --- a/libraries/bezier-rs/src/subpath/core.rs +++ b/libraries/bezier-rs/src/subpath/core.rs @@ -50,16 +50,12 @@ impl Subpath { number_of_curves } - pub fn find_curve_parametric(&self, t: f64) -> (Option, f64) { - assert!((0.0..=1.).contains(&t)); - - let number_of_curves = self.len_segments() as f64; - let scaled_t = t * number_of_curves; - - let target_curve_index = scaled_t.floor() as i32; - let target_curve_t = scaled_t % 1.; - - (self.iter().nth(target_curve_index as usize), target_curve_t) + /// Returns a copy of the bezier segment at the given segment index, if this segment exists. + pub fn get_segment(&self, segment_index: usize) -> Option { + if segment_index >= self.len_segments() { + return None; + } + Some(self[segment_index].to_bezier(&self[(segment_index + 1) % self.len()])) } /// Returns an iterator of the [Bezier]s along the `Subpath`. diff --git a/libraries/bezier-rs/src/subpath/lookup.rs b/libraries/bezier-rs/src/subpath/lookup.rs index 2e0a3e79..7d1336eb 100644 --- a/libraries/bezier-rs/src/subpath/lookup.rs +++ b/libraries/bezier-rs/src/subpath/lookup.rs @@ -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::>(); + 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.)); + } } diff --git a/libraries/bezier-rs/src/subpath/manipulators.rs b/libraries/bezier-rs/src/subpath/manipulators.rs index d781ce8e..79df443f 100644 --- a/libraries/bezier-rs/src/subpath/manipulators.rs +++ b/libraries/bezier-rs/src/subpath/manipulators.rs @@ -1,50 +1,39 @@ use super::*; use crate::consts::MAX_ABSOLUTE_DIFFERENCE; use crate::utils::f64_compare; -use crate::TValue; +use crate::{SubpathTValue, TValue}; impl Subpath { /// Inserts a `ManipulatorGroup` at a certain point along the subpath based on the parametric `t`-value provided. /// Expects `t` to be within the inclusive range `[0, 1]`. - pub fn insert(&mut self, t: TValue) { - match t { - TValue::Parametric(t) => { - assert!((0.0..=1.).contains(&t)); + pub fn insert(&mut self, t: SubpathTValue) { + let (segment_index, t) = self.t_value_to_parametric(t); - let number_of_curves = self.len_segments() as f64; - let scaled_t = t * number_of_curves; - - let target_curve_index = scaled_t.floor() as i32; - let target_curve_t = scaled_t % 1.; - - if f64_compare(target_curve_t, 0., MAX_ABSOLUTE_DIFFERENCE) || f64_compare(target_curve_t, 1., MAX_ABSOLUTE_DIFFERENCE) { - return; - } - - // The only case where `curve` would be `None` is if the provided argument was 1 - // But the above if case would catch that, since `target_curve_t` would be 0. - let curve = self.iter().nth(target_curve_index as usize).unwrap(); - - let [first, second] = curve.split(TValue::Parametric(target_curve_t)); - let new_group = ManipulatorGroup { - anchor: first.end(), - in_handle: first.handle_end(), - out_handle: second.handle_start(), - }; - let number_of_groups = self.manipulator_groups.len() + 1; - self.manipulator_groups.insert((target_curve_index as usize) + 1, new_group); - self.manipulator_groups[(target_curve_index as usize) % number_of_groups].out_handle = first.handle_start(); - self.manipulator_groups[((target_curve_index as usize) + 2) % number_of_groups].in_handle = second.handle_end(); - } - // TODO: change this implementation to Euclidean compute - TValue::Euclidean(_t) => {} - TValue::EuclideanWithinError { t: _, error: _ } => todo!(), + if f64_compare(t, 0., MAX_ABSOLUTE_DIFFERENCE) || f64_compare(t, 1., MAX_ABSOLUTE_DIFFERENCE) { + return; } + + // The only case where `curve` would be `None` is if the provided argument was 1 + // But the above if case would catch that, since `target_curve_t` would be 0. + let curve = self.iter().nth(segment_index).unwrap(); + + let [first, second] = curve.split(TValue::Parametric(t)); + let new_group = ManipulatorGroup { + anchor: first.end(), + in_handle: first.handle_end(), + out_handle: second.handle_start(), + }; + let number_of_groups = self.manipulator_groups.len() + 1; + self.manipulator_groups.insert((segment_index) + 1, new_group); + self.manipulator_groups[segment_index % number_of_groups].out_handle = first.handle_start(); + self.manipulator_groups[(segment_index + 2) % number_of_groups].in_handle = second.handle_end(); } } #[cfg(test)] mod tests { + use crate::utils::SubpathTValue; + use super::*; use glam::DVec2; @@ -94,9 +83,9 @@ mod tests { #[test] fn insert_in_first_segment_of_open_subpath() { let mut subpath = set_up_open_subpath(); - let location = subpath.evaluate(TValue::Parametric(0.2)); + let location = subpath.evaluate(SubpathTValue::GlobalParametric(0.2)); let split_pair = subpath.iter().next().unwrap().split(TValue::Parametric((0.2 * 3.) % 1.)); - subpath.insert(TValue::Parametric(0.2)); + subpath.insert(SubpathTValue::GlobalParametric(0.2)); assert_eq!(subpath.manipulator_groups[1].anchor, location); assert_eq!(split_pair[0], subpath.iter().next().unwrap()); assert_eq!(split_pair[1], subpath.iter().nth(1).unwrap()); @@ -105,9 +94,9 @@ mod tests { #[test] fn insert_in_last_segment_of_open_subpath() { let mut subpath = set_up_open_subpath(); - let location = subpath.evaluate(TValue::Parametric(0.9)); + let location = subpath.evaluate(SubpathTValue::GlobalParametric(0.9)); let split_pair = subpath.iter().nth(2).unwrap().split(TValue::Parametric((0.9 * 3.) % 1.)); - subpath.insert(TValue::Parametric(0.9)); + subpath.insert(SubpathTValue::GlobalParametric(0.9)); assert_eq!(subpath.manipulator_groups[3].anchor, location); assert_eq!(split_pair[0], subpath.iter().nth(2).unwrap()); assert_eq!(split_pair[1], subpath.iter().nth(3).unwrap()); @@ -117,8 +106,8 @@ mod tests { fn insert_at_exisiting_manipulator_group_of_open_subpath() { // This will do nothing to the subpath let mut subpath = set_up_open_subpath(); - let location = subpath.evaluate(TValue::Parametric(0.75)); - subpath.insert(TValue::Parametric(0.75)); + let location = subpath.evaluate(SubpathTValue::GlobalParametric(0.75)); + subpath.insert(SubpathTValue::GlobalParametric(0.75)); assert_eq!(subpath.manipulator_groups[3].anchor, location); assert_eq!(subpath.manipulator_groups.len(), 5); assert_eq!(subpath.len_segments(), 4); @@ -127,9 +116,9 @@ mod tests { #[test] fn insert_at_last_segment_of_closed_subpath() { let mut subpath = set_up_closed_subpath(); - let location = subpath.evaluate(TValue::Parametric(0.9)); + let location = subpath.evaluate(SubpathTValue::GlobalParametric(0.9)); let split_pair = subpath.iter().nth(3).unwrap().split(TValue::Parametric((0.9 * 4.) % 1.)); - subpath.insert(TValue::Parametric(0.9)); + subpath.insert(SubpathTValue::GlobalParametric(0.9)); assert_eq!(subpath.manipulator_groups[4].anchor, location); assert_eq!(split_pair[0], subpath.iter().nth(3).unwrap()); assert_eq!(split_pair[1], subpath.iter().nth(4).unwrap()); @@ -140,8 +129,8 @@ mod tests { fn insert_at_last_manipulator_group_of_closed_subpath() { // This will do nothing to the subpath let mut subpath = set_up_closed_subpath(); - let location = subpath.evaluate(TValue::Parametric(1.)); - subpath.insert(TValue::Parametric(1.)); + let location = subpath.evaluate(SubpathTValue::GlobalParametric(1.)); + subpath.insert(SubpathTValue::GlobalParametric(1.)); assert_eq!(subpath.manipulator_groups[0].anchor, location); assert_eq!(subpath.manipulator_groups.len(), 4); assert!(subpath.closed); diff --git a/libraries/bezier-rs/src/subpath/solvers.rs b/libraries/bezier-rs/src/subpath/solvers.rs index 5dc67571..e2d5a59b 100644 --- a/libraries/bezier-rs/src/subpath/solvers.rs +++ b/libraries/bezier-rs/src/subpath/solvers.rs @@ -1,5 +1,6 @@ use super::*; use crate::consts::MIN_SEPERATION_VALUE; +use crate::utils::SubpathTValue; use crate::TValue; use glam::DVec2; @@ -7,24 +8,12 @@ use glam::DVec2; impl Subpath { /// Calculate the point on the subpath based on the parametric `t`-value provided. /// Expects `t` to be within the inclusive range `[0, 1]`. - pub fn evaluate(&self, t: TValue) -> DVec2 { - match t { - TValue::Parametric(t) => { - assert!((0.0..=1.).contains(&t)); - - if let (Some(curve), target_curve_t) = self.find_curve_parametric(t) { - curve.evaluate(TValue::Parametric(target_curve_t)) - } else { - self.iter().last().unwrap().evaluate(TValue::Parametric(1.)) - } - } - // TODO: change this implementation to Euclidean compute - TValue::Euclidean(_t) => self.iter().next().unwrap().evaluate(TValue::Parametric(0.)), - TValue::EuclideanWithinError { t: _, error: _ } => todo!(), - } + pub fn evaluate(&self, t: SubpathTValue) -> DVec2 { + let (segment_index, t) = self.t_value_to_parametric(t); + self.get_segment(segment_index).unwrap().evaluate(TValue::Parametric(t)) } - /// Calculates the intersection points the subpath has with a given line and returns a list of parameteric `t`-values. + /// Calculates the intersection points the subpath has with a given curve and returns a list of parameteric `t`-values. /// This function expects the following: /// - other: a [Bezier] curve to check intersections against /// - error: an optional f64 value to provide an error bound @@ -54,36 +43,14 @@ impl Subpath { intersection_t_values } - pub fn tangent(&self, t: TValue) -> DVec2 { - match t { - TValue::Parametric(t) => { - assert!((0.0..=1.).contains(&t)); - - if let (Some(curve), target_curve_t) = self.find_curve_parametric(t) { - curve.tangent(TValue::Parametric(target_curve_t)) - } else { - self.iter().last().unwrap().tangent(TValue::Parametric(1.)) - } - } - TValue::Euclidean(_t) => unimplemented!(), - TValue::EuclideanWithinError { t: _, error: _ } => todo!(), - } + pub fn tangent(&self, t: SubpathTValue) -> DVec2 { + let (segment_index, t) = self.t_value_to_parametric(t); + self.get_segment(segment_index).unwrap().tangent(TValue::Parametric(t)) } - pub fn normal(&self, t: TValue) -> DVec2 { - match t { - TValue::Parametric(t) => { - assert!((0.0..=1.).contains(&t)); - - if let (Some(curve), target_curve_t) = self.find_curve_parametric(t) { - curve.normal(TValue::Parametric(target_curve_t)) - } else { - self.iter().last().unwrap().normal(TValue::Parametric(1.)) - } - } - TValue::Euclidean(_t) => unimplemented!(), - TValue::EuclideanWithinError { t: _, error: _ } => todo!(), - } + pub fn normal(&self, t: SubpathTValue) -> DVec2 { + let (segment_index, t) = self.t_value_to_parametric(t); + self.get_segment(segment_index).unwrap().normal(TValue::Parametric(t)) } } @@ -124,16 +91,16 @@ mod tests { ); let t0 = 0.; - assert_eq!(subpath.evaluate(TValue::Parametric(t0)), bezier.evaluate(TValue::Parametric(t0))); + assert_eq!(subpath.evaluate(SubpathTValue::GlobalParametric(t0)), bezier.evaluate(TValue::Parametric(t0))); let t1 = 0.25; - assert_eq!(subpath.evaluate(TValue::Parametric(t1)), bezier.evaluate(TValue::Parametric(t1))); + assert_eq!(subpath.evaluate(SubpathTValue::GlobalParametric(t1)), bezier.evaluate(TValue::Parametric(t1))); let t2 = 0.50; - assert_eq!(subpath.evaluate(TValue::Parametric(t2)), bezier.evaluate(TValue::Parametric(t2))); + assert_eq!(subpath.evaluate(SubpathTValue::GlobalParametric(t2)), bezier.evaluate(TValue::Parametric(t2))); let t3 = 1.; - assert_eq!(subpath.evaluate(TValue::Parametric(t3)), bezier.evaluate(TValue::Parametric(t3))); + assert_eq!(subpath.evaluate(SubpathTValue::GlobalParametric(t3)), bezier.evaluate(TValue::Parametric(t3))); } #[test] @@ -176,7 +143,7 @@ mod tests { let t0 = 0.; assert!(utils::dvec2_compare( - subpath.evaluate(TValue::Parametric(t0)), + subpath.evaluate(SubpathTValue::GlobalParametric(t0)), linear_bezier.evaluate(TValue::Parametric(normalize_t(n, t0))), MAX_ABSOLUTE_DIFFERENCE ) @@ -184,7 +151,7 @@ mod tests { let t1 = 0.25; assert!(utils::dvec2_compare( - subpath.evaluate(TValue::Parametric(t1)), + subpath.evaluate(SubpathTValue::GlobalParametric(t1)), linear_bezier.evaluate(TValue::Parametric(normalize_t(n, t1))), MAX_ABSOLUTE_DIFFERENCE ) @@ -192,7 +159,7 @@ mod tests { let t2 = 0.50; assert!(utils::dvec2_compare( - subpath.evaluate(TValue::Parametric(t2)), + subpath.evaluate(SubpathTValue::GlobalParametric(t2)), quadratic_bezier.evaluate(TValue::Parametric(normalize_t(n, t2))), MAX_ABSOLUTE_DIFFERENCE ) @@ -200,14 +167,19 @@ mod tests { let t3 = 0.75; assert!(utils::dvec2_compare( - subpath.evaluate(TValue::Parametric(t3)), + subpath.evaluate(SubpathTValue::GlobalParametric(t3)), quadratic_bezier.evaluate(TValue::Parametric(normalize_t(n, t3))), MAX_ABSOLUTE_DIFFERENCE ) .all()); let t4 = 1.0; - assert!(utils::dvec2_compare(subpath.evaluate(TValue::Parametric(t4)), quadratic_bezier.evaluate(TValue::Parametric(1.)), MAX_ABSOLUTE_DIFFERENCE).all()); + assert!(utils::dvec2_compare( + subpath.evaluate(SubpathTValue::GlobalParametric(t4)), + quadratic_bezier.evaluate(TValue::Parametric(1.)), + MAX_ABSOLUTE_DIFFERENCE + ) + .all()); // Test closed subpath @@ -216,14 +188,19 @@ mod tests { let t5 = 2. / 3.; assert!(utils::dvec2_compare( - subpath.evaluate(TValue::Parametric(t5)), + subpath.evaluate(SubpathTValue::GlobalParametric(t5)), cubic_bezier.evaluate(TValue::Parametric(normalize_t(n, t5))), MAX_ABSOLUTE_DIFFERENCE ) .all()); let t6 = 1.; - assert!(utils::dvec2_compare(subpath.evaluate(TValue::Parametric(t6)), cubic_bezier.evaluate(TValue::Parametric(1.)), MAX_ABSOLUTE_DIFFERENCE).all()); + assert!(utils::dvec2_compare( + subpath.evaluate(SubpathTValue::GlobalParametric(t6)), + cubic_bezier.evaluate(TValue::Parametric(1.)), + MAX_ABSOLUTE_DIFFERENCE + ) + .all()); } #[test] @@ -272,21 +249,21 @@ mod tests { assert!(utils::dvec2_compare( cubic_bezier.evaluate(TValue::Parametric(cubic_intersections[0])), - subpath.evaluate(TValue::Parametric(subpath_intersections[0])), + subpath.evaluate(SubpathTValue::GlobalParametric(subpath_intersections[0])), MAX_ABSOLUTE_DIFFERENCE ) .all()); assert!(utils::dvec2_compare( quadratic_bezier_1.evaluate(TValue::Parametric(quadratic_1_intersections[0])), - subpath.evaluate(TValue::Parametric(subpath_intersections[1])), + subpath.evaluate(SubpathTValue::GlobalParametric(subpath_intersections[1])), MAX_ABSOLUTE_DIFFERENCE ) .all()); assert!(utils::dvec2_compare( quadratic_bezier_1.evaluate(TValue::Parametric(quadratic_1_intersections[1])), - subpath.evaluate(TValue::Parametric(subpath_intersections[2])), + subpath.evaluate(SubpathTValue::GlobalParametric(subpath_intersections[2])), MAX_ABSOLUTE_DIFFERENCE ) .all()); @@ -339,14 +316,14 @@ mod tests { assert!(utils::dvec2_compare( cubic_bezier.evaluate(TValue::Parametric(cubic_intersections[0])), - subpath.evaluate(TValue::Parametric(subpath_intersections[0])), + subpath.evaluate(SubpathTValue::GlobalParametric(subpath_intersections[0])), MAX_ABSOLUTE_DIFFERENCE ) .all()); assert!(utils::dvec2_compare( quadratic_bezier_1.evaluate(TValue::Parametric(quadratic_1_intersections[0])), - subpath.evaluate(TValue::Parametric(subpath_intersections[1])), + subpath.evaluate(SubpathTValue::GlobalParametric(subpath_intersections[1])), MAX_ABSOLUTE_DIFFERENCE ) .all()); @@ -398,21 +375,21 @@ mod tests { assert!(utils::dvec2_compare( cubic_bezier.evaluate(TValue::Parametric(cubic_intersections[0])), - subpath.evaluate(TValue::Parametric(subpath_intersections[0])), + subpath.evaluate(SubpathTValue::GlobalParametric(subpath_intersections[0])), MAX_ABSOLUTE_DIFFERENCE ) .all()); assert!(utils::dvec2_compare( quadratic_bezier_1.evaluate(TValue::Parametric(quadratic_1_intersections[0])), - subpath.evaluate(TValue::Parametric(subpath_intersections[1])), + subpath.evaluate(SubpathTValue::GlobalParametric(subpath_intersections[1])), MAX_ABSOLUTE_DIFFERENCE ) .all()); assert!(utils::dvec2_compare( quadratic_bezier_1.evaluate(TValue::Parametric(quadratic_1_intersections[1])), - subpath.evaluate(TValue::Parametric(subpath_intersections[2])), + subpath.evaluate(SubpathTValue::GlobalParametric(subpath_intersections[2])), MAX_ABSOLUTE_DIFFERENCE ) .all()); diff --git a/libraries/bezier-rs/src/subpath/structs.rs b/libraries/bezier-rs/src/subpath/structs.rs index 4ee12604..d9ddbdd9 100644 --- a/libraries/bezier-rs/src/subpath/structs.rs +++ b/libraries/bezier-rs/src/subpath/structs.rs @@ -1,3 +1,5 @@ +use super::Bezier; + use glam::DVec2; use std::fmt::{Debug, Formatter, Result}; @@ -22,3 +24,18 @@ impl Debug for ManipulatorGroup { } } } + +impl ManipulatorGroup { + pub fn to_bezier(&self, end_group: &ManipulatorGroup) -> Bezier { + let start = self.anchor; + let end = end_group.anchor; + let out_handle = self.out_handle; + let in_handle = end_group.in_handle; + + match (out_handle, in_handle) { + (Some(handle1), Some(handle2)) => Bezier::from_cubic_dvec2(start, handle1, handle2, end), + (Some(handle), None) | (None, Some(handle)) => Bezier::from_quadratic_dvec2(start, handle, end), + (None, None) => Bezier::from_linear_dvec2(start, end), + } + } +} diff --git a/libraries/bezier-rs/src/subpath/transform.rs b/libraries/bezier-rs/src/subpath/transform.rs index 4d5ce54f..538d3f3c 100644 --- a/libraries/bezier-rs/src/subpath/transform.rs +++ b/libraries/bezier-rs/src/subpath/transform.rs @@ -1,96 +1,85 @@ use super::*; -use crate::TValue; +use crate::utils::SubpathTValue; +use crate::utils::TValue; /// Functionality that transforms Subpaths, such as split, reduce, offset, etc. impl Subpath { /// Returns either one or two Subpaths that result from splitting the original Subpath at the point corresponding to `t`. /// If the original Subpath was closed, a single open Subpath will be returned. /// If the original Subpath was open, two open Subpaths will be returned. - pub fn split(&self, t: TValue) -> (Subpath, Option) { - match t { - TValue::Parametric(t) => { - assert!((0.0..=1.).contains(&t)); + pub fn split(&self, t: SubpathTValue) -> (Subpath, Option) { + let (segment_index, t) = self.t_value_to_parametric(t); + let curve = self.get_segment(segment_index).unwrap(); - let number_of_curves = self.len_segments() as f64; - let scaled_t = t * number_of_curves; + let [first_bezier, second_bezier] = curve.split(TValue::Parametric(t)); - let target_curve_index = scaled_t.floor() as i32; - let target_curve_t = scaled_t % 1.; - let num_manipulator_groups = self.manipulator_groups.len(); + let mut clone = self.manipulator_groups.clone(); + // Split the manipulator group list such that the split location is between the last and first elements of the two split halves + // If the split is on an anchor point, include this anchor point in the first half of the split, except for the first manipulator group which we want in the second group + let (mut first_split, mut second_split) = if !(t == 0. && segment_index == 0) { + let clone2 = clone.split_off(self.len().min(segment_index + 1 + (t == 1.) as usize)); + (clone, clone2) + } else { + (vec![], clone) + }; - // The only case where `curve` would be `None` is if the provided argument was 1 - let optional_curve = self.iter().nth(target_curve_index as usize); - let curve = optional_curve.unwrap_or_else(|| self.iter().last().unwrap()); - - let [first_bezier, second_bezier] = curve.split(TValue::Parametric(if t == 1. { t } else { target_curve_t })); - - let mut clone = self.manipulator_groups.clone(); - let (mut first_split, mut second_split) = if t > 0. { - let clone2 = clone.split_off(num_manipulator_groups.min((target_curve_index as usize) + 1)); - (clone, clone2) - } else { - (vec![], clone) - }; - - if self.closed && (t == 0. || t == 1.) { - // The entire vector of manipulator groups will be in the second_split because target_curve_index == 0. - // Add a new manipulator group with the same anchor as the first node to represent the end of the now opened subpath - let last_curve = self.iter().last().unwrap(); - first_split.push(ManipulatorGroup { - anchor: first_bezier.end(), - in_handle: last_curve.handle_end(), - out_handle: None, - }); - } else { - if !first_split.is_empty() { - let num_elements = first_split.len(); - first_split[num_elements - 1].out_handle = first_bezier.handle_start(); - } - - if !second_split.is_empty() { - second_split[0].in_handle = second_bezier.handle_end(); - } - - // Push new manipulator groups to represent the location of the split at the end of the first group and at the start of the second - // If the split was at a manipulator group's anchor, add only one manipulator group - // Add it to the first list when the split location is on the first manipulator group, otherwise add to the second list - if target_curve_t != 0. || t == 0. { - first_split.push(ManipulatorGroup { - anchor: first_bezier.end(), - in_handle: first_bezier.handle_end(), - out_handle: None, - }); - } - - if t != 0. { - second_split.insert( - 0, - ManipulatorGroup { - anchor: second_bezier.start(), - in_handle: None, - out_handle: second_bezier.handle_start(), - }, - ); - } - } - - if self.closed { - // "Rotate" the manipulator groups list so that the split point becomes the start and end of the open subpath - second_split.append(&mut first_split); - (Subpath::new(second_split, false), None) - } else { - (Subpath::new(first_split, false), Some(Subpath::new(second_split, false))) - } + // If the subpath is closed and the split point is the start or end of the Subpath + if self.closed && ((t == 0. && segment_index == 0) || (t == 1. && segment_index == self.len_segments() - 1)) { + // The entire vector of manipulator groups will be in the second_split + // Add a new manipulator group with the same anchor as the first node to represent the end of the now opened subpath + let last_curve = self.iter().last().unwrap(); + first_split.push(ManipulatorGroup { + anchor: first_bezier.end(), + in_handle: last_curve.handle_end(), + out_handle: None, + }); + } else { + if !first_split.is_empty() { + let num_elements = first_split.len(); + first_split[num_elements - 1].out_handle = first_bezier.handle_start(); } - // TODO: change this implementation to Euclidean compute - TValue::Euclidean(_t) => todo!(), - TValue::EuclideanWithinError { t: _, error: _ } => todo!(), + + if !second_split.is_empty() { + second_split[0].in_handle = second_bezier.handle_end(); + } + + // Push new manipulator groups to represent the location of the split at the end of the first group and at the start of the second + // If the split was at a manipulator group's anchor, add only one manipulator group + // Add it to the first list when the split location is on the first manipulator group, otherwise add to the second list + if (t % 1. != 0.) || segment_index == 0 { + first_split.push(ManipulatorGroup { + anchor: first_bezier.end(), + in_handle: first_bezier.handle_end(), + out_handle: None, + }); + } + + if !(t == 0. && segment_index == 0) { + second_split.insert( + 0, + ManipulatorGroup { + anchor: second_bezier.start(), + in_handle: None, + out_handle: second_bezier.handle_start(), + }, + ); + } + } + + if self.closed { + // "Rotate" the manipulator groups list so that the split point becomes the start and end of the open subpath + second_split.append(&mut first_split); + (Subpath::new(second_split, false), None) + } else { + (Subpath::new(first_split, false), Some(Subpath::new(second_split, false))) } } } #[cfg(test)] mod tests { + use crate::utils::SubpathTValue; + use super::*; use glam::DVec2; @@ -140,9 +129,9 @@ mod tests { #[test] fn split_an_open_subpath() { let subpath = set_up_open_subpath(); - let location = subpath.evaluate(TValue::Parametric(0.2)); + let location = subpath.evaluate(SubpathTValue::GlobalParametric(0.2)); let split_pair = subpath.iter().next().unwrap().split(TValue::Parametric((0.2 * 3.) % 1.)); - let (first, second) = subpath.split(TValue::Parametric(0.2)); + let (first, second) = subpath.split(SubpathTValue::GlobalParametric(0.2)); assert!(second.is_some()); let second = second.unwrap(); assert_eq!(first.manipulator_groups[1].anchor, location); @@ -154,9 +143,9 @@ mod tests { #[test] fn split_at_start_of_an_open_subpath() { let subpath = set_up_open_subpath(); - let location = subpath.evaluate(TValue::Parametric(0.)); + let location = subpath.evaluate(SubpathTValue::GlobalParametric(0.)); let split_pair = subpath.iter().next().unwrap().split(TValue::Parametric(0.)); - let (first, second) = subpath.split(TValue::Parametric(0.)); + let (first, second) = subpath.split(SubpathTValue::GlobalParametric(0.)); assert!(second.is_some()); let second = second.unwrap(); assert_eq!( @@ -175,9 +164,9 @@ mod tests { #[test] fn split_at_end_of_an_open_subpath() { let subpath = set_up_open_subpath(); - let location = subpath.evaluate(TValue::Parametric(1.)); + let location = subpath.evaluate(SubpathTValue::GlobalParametric(1.)); let split_pair = subpath.iter().last().unwrap().split(TValue::Parametric(1.)); - let (first, second) = subpath.split(TValue::Parametric(1.)); + let (first, second) = subpath.split(SubpathTValue::GlobalParametric(1.)); assert!(second.is_some()); let second = second.unwrap(); assert_eq!(first.manipulator_groups[3].anchor, location); @@ -196,9 +185,9 @@ mod tests { #[test] fn split_a_closed_subpath() { let subpath = set_up_closed_subpath(); - let location = subpath.evaluate(TValue::Parametric(0.2)); + let location = subpath.evaluate(SubpathTValue::GlobalParametric(0.2)); let split_pair = subpath.iter().next().unwrap().split(TValue::Parametric((0.2 * 4.) % 1.)); - let (first, second) = subpath.split(TValue::Parametric(0.2)); + let (first, second) = subpath.split(SubpathTValue::GlobalParametric(0.2)); assert!(second.is_none()); assert_eq!(first.manipulator_groups[0].anchor, location); assert_eq!(first.manipulator_groups[5].anchor, location); @@ -210,8 +199,8 @@ mod tests { #[test] fn split_at_start_of_a_closed_subpath() { let subpath = set_up_closed_subpath(); - let location = subpath.evaluate(TValue::Parametric(0.)); - let (first, second) = subpath.split(TValue::Parametric(0.)); + let location = subpath.evaluate(SubpathTValue::GlobalParametric(0.)); + let (first, second) = subpath.split(SubpathTValue::GlobalParametric(0.)); assert!(second.is_none()); assert_eq!(first.manipulator_groups[0].anchor, location); assert_eq!(first.manipulator_groups[4].anchor, location); @@ -224,8 +213,8 @@ mod tests { #[test] fn split_at_end_of_a_closed_subpath() { let subpath = set_up_closed_subpath(); - let location = subpath.evaluate(TValue::Parametric(1.)); - let (first, second) = subpath.split(TValue::Parametric(1.)); + let location = subpath.evaluate(SubpathTValue::GlobalParametric(1.)); + let (first, second) = subpath.split(SubpathTValue::GlobalParametric(1.)); assert!(second.is_none()); assert_eq!(first.manipulator_groups[0].anchor, location); assert_eq!(first.manipulator_groups[4].anchor, location); diff --git a/libraries/bezier-rs/src/utils.rs b/libraries/bezier-rs/src/utils.rs index f101fee0..a8ecd78d 100644 --- a/libraries/bezier-rs/src/utils.rs +++ b/libraries/bezier-rs/src/utils.rs @@ -18,6 +18,16 @@ pub enum TValue { EuclideanWithinError { t: f64, error: f64 }, } +#[derive(Copy, Clone, PartialEq)] +pub enum SubpathTValue { + Parametric { segment_index: usize, t: f64 }, + GlobalParametric(f64), + Euclidean { segment_index: usize, t: f64 }, + GlobalEuclidean(f64), + EuclideanWithinError { segment_index: usize, t: f64, error: f64 }, + GlobalEuclideanWithinError { t: f64, error: f64 }, +} + /// Helper to perform the computation of a and c, where b is the provided point on the curve. /// Given the correct power of `t` and `(1-t)`, the computation is the same for quadratic and cubic cases. /// Relevant derivation and the definitions of a, b, and c can be found in [the projection identity section](https://pomax.github.io/bezierinfo/#abc) of Pomax's bezier curve primer. diff --git a/website/other/bezier-rs-demos/src/features/subpath-features.ts b/website/other/bezier-rs-demos/src/features/subpath-features.ts index fedefa0a..92eb6bc3 100644 --- a/website/other/bezier-rs-demos/src/features/subpath-features.ts +++ b/website/other/bezier-rs-demos/src/features/subpath-features.ts @@ -10,8 +10,7 @@ const subpathFeatures = { name: "Insert", callback: (subpath: WasmSubpathInstance, options: Record, _: undefined, tVariant: TVariant): string => subpath.insert(options.t, tVariant), sliderOptions: [tSliderOptions], - // TODO: Uncomment this after implementing the Euclidean version - // chooseTVariant: true, + chooseTVariant: true, }, length: { name: "Length", @@ -31,13 +30,15 @@ const subpathFeatures = { }, tangent: { name: "Tangent", - callback: (subpath: WasmSubpathInstance, options: Record): string => subpath.tangent(options.t), + callback: (subpath: WasmSubpathInstance, options: Record, _: undefined, tVariant: TVariant): string => subpath.tangent(options.t, tVariant), sliderOptions: [tSliderOptions], + chooseTVariant: true, }, normal: { name: "Normal", - callback: (subpath: WasmSubpathInstance, options: Record): string => subpath.normal(options.t), + callback: (subpath: WasmSubpathInstance, options: Record, _: undefined, tVariant: TVariant): string => subpath.normal(options.t, tVariant), sliderOptions: [tSliderOptions], + chooseTVariant: true, }, "intersect-linear": { name: "Intersect (Line Segment)", @@ -70,8 +71,7 @@ const subpathFeatures = { name: "Split", callback: (subpath: WasmSubpathInstance, options: Record, _: undefined, tVariant: TVariant): string => subpath.split(options.t, tVariant), sliderOptions: [tSliderOptions], - // TODO: Uncomment this after implementing the Euclidean version - // chooseTVariant: true, + chooseTVariant: true, }, }; diff --git a/website/other/bezier-rs-demos/wasm/src/subpath.rs b/website/other/bezier-rs-demos/wasm/src/subpath.rs index 48add023..244944aa 100644 --- a/website/other/bezier-rs-demos/wasm/src/subpath.rs +++ b/website/other/bezier-rs-demos/wasm/src/subpath.rs @@ -1,6 +1,6 @@ use crate::svg_drawing::*; -use bezier_rs::{Bezier, ManipulatorGroup, ProjectionOptions, Subpath, TValue}; +use bezier_rs::{Bezier, ManipulatorGroup, ProjectionOptions, Subpath, SubpathTValue}; use glam::DVec2; use std::fmt::Write; @@ -12,6 +12,14 @@ pub struct WasmSubpath(Subpath); const SCALE_UNIT_VECTOR_FACTOR: f64 = 50.; +fn parse_t_variant(t_variant: &String, t: f64) -> SubpathTValue { + match t_variant.as_str() { + "Parametric" => SubpathTValue::GlobalParametric(t), + "Euclidean" => SubpathTValue::GlobalEuclidean(t), + _ => panic!("Unexpected TValue string: '{}'", t_variant), + } +} + #[wasm_bindgen] impl WasmSubpath { /// Expects js_points to be an unbounded list of triples, where each item is a tuple of floats. @@ -58,21 +66,12 @@ impl WasmSubpath { pub fn insert(&self, t: f64, t_variant: String) -> String { let mut subpath = self.0.clone(); - let point = match t_variant.as_str() { - "Euclidean" => { - let parameter = TValue::Euclidean(t); - subpath.insert(parameter); - self.0.evaluate(parameter) - } - "Parametric" => { - let parameter = TValue::Parametric(t); - subpath.insert(parameter); - self.0.evaluate(parameter) - } - _ => panic!("Unexpected TValue string: '{}'", t_variant), - }; - let point_text = draw_circle(point, 4., RED, 1.5, WHITE); + let t = parse_t_variant(&t_variant, t); + subpath.insert(t); + let point = self.0.evaluate(t); + + let point_text = draw_circle(point, 4., RED, 1.5, WHITE); wrap_svg_tag(format!("{}{}", WasmSubpath(subpath).to_default_svg(), point_text)) } @@ -82,18 +81,18 @@ impl WasmSubpath { } pub fn evaluate(&self, t: f64, t_variant: String) -> String { - let point = match t_variant.as_str() { - "Euclidean" => self.0.evaluate(TValue::Euclidean(t)), - "Parametric" => self.0.evaluate(TValue::Parametric(t)), - _ => panic!("Unexpected TValue string: '{}'", t_variant), - }; + let t = parse_t_variant(&t_variant, t); + let point = self.0.evaluate(t); + let point_text = draw_circle(point, 4., RED, 1.5, WHITE); wrap_svg_tag(format!("{}{}", self.to_default_svg(), point_text)) } - pub fn tangent(&self, t: f64) -> String { - let intersection_point = self.0.evaluate(TValue::Parametric(t)); - let tangent_point = self.0.tangent(TValue::Parametric(t)); + pub fn tangent(&self, t: f64, t_variant: String) -> String { + let t = parse_t_variant(&t_variant, t); + + let intersection_point = self.0.evaluate(t); + let tangent_point = self.0.tangent(t); let tangent_end = intersection_point + tangent_point * SCALE_UNIT_VECTOR_FACTOR; let point_text = draw_circle(intersection_point, 4., RED, 1.5, WHITE); @@ -102,9 +101,11 @@ impl WasmSubpath { wrap_svg_tag(format!("{}{}{}{}", self.to_default_svg(), point_text, line_text, tangent_end_point)) } - pub fn normal(&self, t: f64) -> String { - let intersection_point = self.0.evaluate(TValue::Parametric(t)); - let normal_point = self.0.normal(TValue::Parametric(t)); + pub fn normal(&self, t: f64, t_variant: String) -> String { + let t = parse_t_variant(&t_variant, t); + + let intersection_point = self.0.evaluate(t); + let normal_point = self.0.normal(t); let normal_end = intersection_point + normal_point * SCALE_UNIT_VECTOR_FACTOR; let point_text = draw_circle(intersection_point, 4., RED, 1.5, WHITE); @@ -115,7 +116,7 @@ impl WasmSubpath { pub fn project(&self, x: f64, y: f64) -> String { let (segment_index, projected_t) = self.0.project(DVec2::new(x, y), ProjectionOptions::default()).unwrap(); - let projected_point = self.0.evaluate(TValue::Parametric((segment_index as f64 + projected_t) / (self.0.len_segments() as f64))); + let projected_point = self.0.evaluate(SubpathTValue::Parametric { segment_index, t: projected_t }); let subpath_svg = self.to_default_svg(); let content = format!("{subpath_svg}{}", draw_line(projected_point.x, projected_point.y, x, y, RED, 1.),); @@ -143,7 +144,7 @@ impl WasmSubpath { .intersections(&line, None, None) .iter() .map(|intersection_t| { - let point = self.0.evaluate(TValue::Parametric(*intersection_t)); + let point = self.0.evaluate(SubpathTValue::GlobalParametric(*intersection_t)); draw_circle(point, 4., RED, 1.5, WHITE) }) .fold(String::new(), |acc, item| format!("{acc}{item}")); @@ -172,7 +173,7 @@ impl WasmSubpath { .intersections(&line, None, None) .iter() .map(|intersection_t| { - let point = self.0.evaluate(TValue::Parametric(*intersection_t)); + let point = self.0.evaluate(SubpathTValue::GlobalParametric(*intersection_t)); draw_circle(point, 4., RED, 1.5, WHITE) }) .fold(String::new(), |acc, item| format!("{acc}{item}")); @@ -201,7 +202,7 @@ impl WasmSubpath { .intersections(&line, None, None) .iter() .map(|intersection_t| { - let point = self.0.evaluate(TValue::Parametric(*intersection_t)); + let point = self.0.evaluate(SubpathTValue::GlobalParametric(*intersection_t)); draw_circle(point, 4., RED, 1.5, WHITE) }) .fold(String::new(), |acc, item| format!("{acc}{item}")); @@ -210,11 +211,8 @@ impl WasmSubpath { } pub fn split(&self, t: f64, t_variant: String) -> String { - let (main_subpath, optional_subpath) = match t_variant.as_str() { - "Euclidean" => self.0.split(TValue::Euclidean(t)), - "Parametric" => self.0.split(TValue::Parametric(t)), - _ => panic!("Unexpected ComputeType string: '{}'", t_variant), - }; + let t = parse_t_variant(&t_variant, t); + let (main_subpath, optional_subpath) = self.0.split(t); let mut main_subpath_svg = String::new(); let mut other_subpath_svg = String::new();