diff --git a/editor/src/messages/tool/common_functionality/shape_editor.rs b/editor/src/messages/tool/common_functionality/shape_editor.rs index 19985f83..795f269d 100644 --- a/editor/src/messages/tool/common_functionality/shape_editor.rs +++ b/editor/src/messages/tool/common_functionality/shape_editor.rs @@ -1,6 +1,6 @@ use crate::messages::prelude::*; -use bezier_rs::ComputeType; +use bezier_rs::TValue; use document_legacy::{LayerId, Operation}; use graphene_std::vector::consts::ManipulatorType; use graphene_std::vector::manipulator_group::ManipulatorGroup; @@ -296,7 +296,7 @@ impl ShapeEditor { for bezier_id in document.layer(layer_path).ok()?.as_subpath()?.bezier_iter() { let bezier = bezier_id.internal; let t = bezier.project(layer_pos, projection_options); - let layerspace = bezier.evaluate(ComputeType::Parametric(t)); + let layerspace = bezier.evaluate(TValue::Parametric(t)); let screenspace = transform.transform_point2(layerspace); let distance_squared = screenspace.distance_squared(position); @@ -314,7 +314,7 @@ impl ShapeEditor { pub fn split(&self, document: &Document, position: glam::DVec2, tolerance: f64, responses: &mut VecDeque) { for layer_path in &self.selected_layers { if let Some((bezier_id, t)) = self.closest_segment(document, layer_path, position, tolerance) { - let [first, second] = bezier_id.internal.split(t); + let [first, second] = bezier_id.internal.split(TValue::Parametric(t)); // Adjust the first manipulator group's out handle let out_handle = Operation::SetManipulatorPoints { diff --git a/libraries/bezier-rs/src/bezier/core.rs b/libraries/bezier-rs/src/bezier/core.rs index 3b26ea63..140f4860 100644 --- a/libraries/bezier-rs/src/bezier/core.rs +++ b/libraries/bezier-rs/src/bezier/core.rs @@ -203,7 +203,7 @@ impl Bezier { #[cfg(test)] mod tests { - use crate::utils::ComputeType; + use crate::utils::TValue; use super::compare::compare_points; use super::*; @@ -215,13 +215,13 @@ mod tests { let p3 = DVec2::new(160., 170.); let bezier1 = Bezier::quadratic_through_points(p1, p2, p3, None); - assert!(compare_points(bezier1.evaluate(ComputeType::Parametric(0.5)), p2)); + assert!(compare_points(bezier1.evaluate(TValue::Parametric(0.5)), p2)); let bezier2 = Bezier::quadratic_through_points(p1, p2, p3, Some(0.8)); - assert!(compare_points(bezier2.evaluate(ComputeType::Parametric(0.8)), p2)); + assert!(compare_points(bezier2.evaluate(TValue::Parametric(0.8)), p2)); let bezier3 = Bezier::quadratic_through_points(p1, p2, p3, Some(0.)); - assert!(compare_points(bezier3.evaluate(ComputeType::Parametric(0.)), p2)); + assert!(compare_points(bezier3.evaluate(TValue::Parametric(0.)), p2)); } #[test] @@ -231,12 +231,12 @@ mod tests { let p3 = DVec2::new(160., 160.); let bezier1 = Bezier::cubic_through_points(p1, p2, p3, Some(0.3), Some(10.)); - assert!(compare_points(bezier1.evaluate(ComputeType::Parametric(0.3)), p2)); + assert!(compare_points(bezier1.evaluate(TValue::Parametric(0.3)), p2)); let bezier2 = Bezier::cubic_through_points(p1, p2, p3, Some(0.8), Some(91.7)); - assert!(compare_points(bezier2.evaluate(ComputeType::Parametric(0.8)), p2)); + assert!(compare_points(bezier2.evaluate(TValue::Parametric(0.8)), p2)); let bezier3 = Bezier::cubic_through_points(p1, p2, p3, Some(0.), Some(91.7)); - assert!(compare_points(bezier3.evaluate(ComputeType::Parametric(0.)), p2)); + assert!(compare_points(bezier3.evaluate(TValue::Parametric(0.)), p2)); } } diff --git a/libraries/bezier-rs/src/bezier/lookup.rs b/libraries/bezier-rs/src/bezier/lookup.rs index 130b1d74..1158c416 100644 --- a/libraries/bezier-rs/src/bezier/lookup.rs +++ b/libraries/bezier-rs/src/bezier/lookup.rs @@ -1,9 +1,49 @@ -use crate::utils::{f64_compare, ComputeType}; +use crate::utils::{f64_compare, TValue}; use super::*; /// Functionality relating to looking up properties of the `Bezier` or points along the `Bezier`. impl Bezier { + /// Convert a euclidean distance ratio along the `Bezier` curve to a parametric `t`-value. + pub fn euclidean_to_parametric(&self, ratio: f64, error: f64) -> f64 { + let mut low = 0.; + let mut mid = 0.; + let mut high = 1.; + let total_length = self.length(None); + + while low < high { + mid = (low + high) / 2.; + let test_ratio = self.trim(TValue::Parametric(0.), TValue::Parametric(mid)).length(None) / total_length; + if f64_compare(test_ratio, ratio, error) { + break; + } else if test_ratio < ratio { + low = mid; + } else { + high = mid; + } + } + + mid + } + + /// Convert a [TValue] to a parametric `t`-value. + pub(crate) fn t_value_to_parametric(&self, t: TValue) -> f64 { + match t { + TValue::Parametric(t) => { + assert!((0.0..=1.).contains(&t)); + t + } + TValue::Euclidean(t) => { + assert!((0.0..=1.).contains(&t)); + self.euclidean_to_parametric(t, DEFAULT_EUCLIDEAN_ERROR_BOUND) + } + TValue::EuclideanWithinError { t, error } => { + assert!((0.0..=1.).contains(&t)); + self.euclidean_to_parametric(t, error) + } + } + } + /// Calculate the point on the curve based on the `t`-value provided. pub(crate) fn unrestricted_parametric_evaluate(&self, t: f64) -> DVec2 { // Basis code based off of pseudocode found here: . @@ -23,48 +63,11 @@ impl Bezier { } } - /// Calculate the point along the curve that is a factor of `d` away from the start. - pub(crate) fn unrestricted_euclidean_evaluate(&self, d: f64, error: f64) -> DVec2 { - if let BezierHandles::Linear = self.handles { - return self.unrestricted_parametric_evaluate(d); - } - - let mut low = 0.; - let mut mid = 0.; - let mut high = 1.; - let total_length = self.length(None); - - while low < high { - mid = (low + high) / 2.; - let test_d = self.trim(0., mid).length(None) / total_length; - if f64_compare(test_d, d, error) { - break; - } else if test_d < d { - low = mid; - } else { - high = mid; - } - } - self.unrestricted_parametric_evaluate(mid) - } - - /// Calculate the point on the curve based on the `t`-value provided. + /// Calculate the coordinates of the point `t` along the curve. /// Expects `t` to be within the inclusive range `[0, 1]`. - pub fn evaluate(&self, t: ComputeType) -> DVec2 { - match t { - ComputeType::Parametric(t) => { - assert!((0.0..=1.).contains(&t)); - self.unrestricted_parametric_evaluate(t) - } - ComputeType::Euclidean(t) => { - assert!((0.0..=1.).contains(&t)); - self.unrestricted_euclidean_evaluate(t, 0.0001) - } - ComputeType::EuclideanWithinError { t, epsilon } => { - assert!((0.0..=1.).contains(&t)); - self.unrestricted_euclidean_evaluate(t, epsilon) - } - } + pub fn evaluate(&self, t: TValue) -> DVec2 { + let t = self.t_value_to_parametric(t); + self.unrestricted_parametric_evaluate(t) } /// Return a selection of equidistant points on the bezier curve. @@ -75,7 +78,7 @@ impl Bezier { let mut steps_array = Vec::with_capacity(steps_unwrapped + 1); for t in 0..steps_unwrapped + 1 { - steps_array.push(self.evaluate(ComputeType::Parametric(f64::from(t as i32) * ratio))) + steps_array.push(self.evaluate(TValue::Parametric(f64::from(t as i32) * ratio))) } steps_array @@ -107,7 +110,7 @@ impl Bezier { } } - /// Returns the `t` value that corresponds to the closest point on the curve to the provided point. + /// Returns the parametric `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) -> f64 { let ProjectionOptions { @@ -162,7 +165,7 @@ impl Bezier { if step_index == 0 { distance = *table_distance; } else { - distance = point.distance(self.evaluate(ComputeType::Parametric(iterator_t))); + distance = point.distance(self.evaluate(TValue::Parametric(iterator_t))); *table_distance = distance; } if distance < new_minimum_distance { @@ -212,17 +215,17 @@ mod tests { let p4 = DVec2::new(30., 21.); let bezier1 = Bezier::from_quadratic_dvec2(p1, p2, p3); - assert_eq!(bezier1.evaluate(ComputeType::Parametric(0.5)), DVec2::new(12.5, 6.25)); + assert_eq!(bezier1.evaluate(TValue::Parametric(0.5)), DVec2::new(12.5, 6.25)); let bezier2 = Bezier::from_cubic_dvec2(p1, p2, p3, p4); - assert_eq!(bezier2.evaluate(ComputeType::Parametric(0.5)), DVec2::new(16.5, 9.625)); + assert_eq!(bezier2.evaluate(TValue::Parametric(0.5)), DVec2::new(16.5, 9.625)); } #[test] fn test_compute_lookup_table() { let bezier1 = Bezier::from_quadratic_coordinates(10., 10., 30., 30., 50., 10.); let lookup_table1 = bezier1.compute_lookup_table(Some(2)); - assert_eq!(lookup_table1, vec![bezier1.start(), bezier1.evaluate(ComputeType::Parametric(0.5)), bezier1.end()]); + assert_eq!(lookup_table1, vec![bezier1.start(), bezier1.evaluate(TValue::Parametric(0.5)), bezier1.end()]); let bezier2 = Bezier::from_cubic_coordinates(10., 10., 30., 30., 70., 70., 90., 10.); let lookup_table2 = bezier2.compute_lookup_table(Some(4)); @@ -230,9 +233,9 @@ mod tests { lookup_table2, vec![ bezier2.start(), - bezier2.evaluate(ComputeType::Parametric(0.25)), - bezier2.evaluate(ComputeType::Parametric(0.50)), - bezier2.evaluate(ComputeType::Parametric(0.75)), + bezier2.evaluate(TValue::Parametric(0.25)), + bezier2.evaluate(TValue::Parametric(0.50)), + bezier2.evaluate(TValue::Parametric(0.75)), bezier2.end() ] ); diff --git a/libraries/bezier-rs/src/bezier/solvers.rs b/libraries/bezier-rs/src/bezier/solvers.rs index 40097b92..4b5c3a34 100644 --- a/libraries/bezier-rs/src/bezier/solvers.rs +++ b/libraries/bezier-rs/src/bezier/solvers.rs @@ -1,15 +1,16 @@ use super::*; -use crate::utils::ComputeType; +use crate::utils::TValue; use glam::DMat2; use std::ops::Range; /// Functionality that solve for various curve information such as derivative, tangent, intersect, etc. impl Bezier { - /// Returns a list of lists of points representing the De Casteljau points for all iterations at the point corresponding to `t` using De Casteljau's algorithm. + /// Returns a list of lists of points representing the De Casteljau points for all iterations at the point `t` along the curve using De Casteljau's algorithm. /// The `i`th element of the list represents the set of points in the `i`th iteration. /// More information on the algorithm can be found in the [De Casteljau section](https://pomax.github.io/bezierinfo/#decasteljau) in Pomax's primer. - pub fn de_casteljau_points(&self, t: f64) -> Vec> { + pub fn de_casteljau_points(&self, t: TValue) -> Vec> { + let t = self.t_value_to_parametric(t); let bezier_points = match self.handles { BezierHandles::Linear => vec![self.start, self.end], BezierHandles::Quadratic { handle } => vec![self.start, handle, self.end], @@ -30,7 +31,7 @@ impl Bezier { de_casteljau_points } - /// Returns a Bezier representing the derivative of the original curve. + /// Returns a [Bezier] representing the derivative of the original curve. /// - This function returns `None` for a linear segment. pub fn derivative(&self) -> Option { match self.handles { @@ -49,27 +50,29 @@ impl Bezier { } } - /// Returns a normalized unit vector representing the tangent at the point designated by `t` on the curve. - pub fn tangent(&self, t: f64) -> DVec2 { + /// Returns a normalized unit vector representing the tangent at the point `t` along the curve. + pub fn tangent(&self, t: TValue) -> DVec2 { + let t = self.t_value_to_parametric(t); match self.handles { BezierHandles::Linear => self.end - self.start, - _ => self.derivative().unwrap().evaluate(ComputeType::Parametric(t)), + _ => self.derivative().unwrap().evaluate(TValue::Parametric(t)), } .normalize() } - /// Returns a normalized unit vector representing the direction of the normal at the point designated by `t` on the curve. - pub fn normal(&self, t: f64) -> DVec2 { + /// Returns a normalized unit vector representing the direction of the normal at the point `t` along the curve. + pub fn normal(&self, t: TValue) -> DVec2 { self.tangent(t).perp() } - /// Returns the curvature, a scalar value for the derivative at the given `t`-value along the curve. + /// Returns the curvature, a scalar value for the derivative at the point `t` along the curve. /// Curvature is 1 over the radius of a circle with an equivalent derivative. - pub fn curvature(&self, t: f64) -> f64 { + pub fn curvature(&self, t: TValue) -> f64 { + let t = self.t_value_to_parametric(t); let (d, dd) = match &self.derivative() { Some(first_derivative) => match first_derivative.derivative() { - Some(second_derivative) => (first_derivative.evaluate(ComputeType::Parametric(t)), second_derivative.evaluate(ComputeType::Parametric(t))), - None => (first_derivative.evaluate(ComputeType::Parametric(t)), first_derivative.end - first_derivative.start), + Some(second_derivative) => (first_derivative.evaluate(TValue::Parametric(t)), second_derivative.evaluate(TValue::Parametric(t))), + None => (first_derivative.evaluate(TValue::Parametric(t)), first_derivative.end - first_derivative.start), }, None => (self.end - self.start, DVec2::new(0., 0.)), }; @@ -129,7 +132,7 @@ impl Bezier { let extrema = self.local_extrema(); for t_values in extrema { for t in t_values { - let point = self.evaluate(ComputeType::Parametric(t)); + let point = self.evaluate(TValue::Parametric(t)); // Update bounding box if new min/max is found. endpoints_min = endpoints_min.min(point); endpoints_max = endpoints_max.max(point); @@ -178,7 +181,7 @@ impl Bezier { } } - /// Returns list of `t`-values representing the inflection points of the curve. + /// Returns list of parametric `t`-values representing the inflection points of the curve. /// The list of `t`-values returned are filtered such that they fall within the range `[0, 1]`. pub fn inflections(&self) -> Vec { self.unrestricted_inflections().into_iter().filter(|&t| t > 0. && t < 1.).collect::>() @@ -213,8 +216,8 @@ impl Bezier { } // Split curves in half and repeat with the combinations of the two halves of each curve - let [split_1_a, split_1_b] = self.split(0.5); - let [split_2_a, split_2_b] = other.split(0.5); + let [split_1_a, split_1_b] = self.split(TValue::Parametric(0.5)); + let [split_2_a, split_2_b] = other.split(TValue::Parametric(0.5)); [ split_1_a.intersections_between_subcurves(self_start_t..self_mid_t, &split_2_a, other_start_t..other_mid_t, error), @@ -229,7 +232,7 @@ impl Bezier { } // TODO: Use an `impl Iterator` return type instead of a `Vec` - /// Returns a list of filtered `t` values that correspond to intersection points between the current bezier curve and the provided one + /// Returns a list of filtered parametric `t` values that correspond to intersection points between the current bezier curve and the provided one /// such that the difference between adjacent `t` values in sorted order is greater than some minimum seperation value. If the difference /// between 2 adjacent `t` values is lesss than the minimum difference, the filtering takes the larger `t` value and discards the smaller `t` value. /// The returned `t` values are with respect to the current bezier, not the provided parameter. @@ -242,8 +245,6 @@ impl Bezier { let mut intersection_t_values = self.unfiltered_intersections(other, error); intersection_t_values.sort_by(|a, b| a.partial_cmp(b).unwrap()); - // println!("<<<<< intersection_t_values :: {:?}", intersection_t_values); - intersection_t_values.iter().fold(Vec::new(), |mut accumulator, t| { if !accumulator.is_empty() && (accumulator.last().unwrap() - t).abs() < minimum_seperation.unwrap_or(MIN_SEPERATION_VALUE) { accumulator.pop(); @@ -334,7 +335,7 @@ impl Bezier { } // TODO: Use an `impl Iterator` return type instead of a `Vec` - /// Returns a list of `t` values that correspond to the self intersection points of the current bezier curve. For each intersection point, the returned `t` value is the smaller of the two that correspond to the point. + /// Returns a list of parametric `t` values that correspond to the self intersection points of the current bezier curve. For each intersection point, the returned `t` value is the smaller of the two that correspond to the point. /// - `error` - For intersections with non-linear beziers, `error` defines the threshold for bounding boxes to be considered an intersection point. pub fn self_intersections(&self, error: Option) -> Vec<[f64; 2]> { if self.handles == BezierHandles::Linear || matches!(self.handles, BezierHandles::Quadratic { .. }) { @@ -362,7 +363,7 @@ impl Bezier { .collect() } - /// Returns a list of `t` values that correspond to the intersection points between the curve and a rectangle defined by opposite corners. + /// Returns a list of parametric `t` values that correspond to the intersection points between the curve and a rectangle defined by opposite corners. pub fn rectangle_intersections(&self, corner1: DVec2, corner2: DVec2) -> Vec { [ Bezier::from_linear_coordinates(corner1.x, corner1.y, corner2.x, corner1.y), @@ -384,7 +385,7 @@ mod tests { #[test] fn test_de_casteljau_points() { let bezier = Bezier::from_cubic_coordinates(0., 0., 0., 100., 100., 100., 100., 0.); - let de_casteljau_points = bezier.de_casteljau_points(0.5); + let de_casteljau_points = bezier.de_casteljau_points(TValue::Parametric(0.5)); let expected_de_casteljau_points = vec![ vec![DVec2::new(0., 0.), DVec2::new(0., 100.), DVec2::new(100., 100.), DVec2::new(100., 0.)], vec![DVec2::new(0., 50.), DVec2::new(50., 100.), DVec2::new(100., 50.)], @@ -393,7 +394,7 @@ mod tests { ]; assert_eq!(&de_casteljau_points, &expected_de_casteljau_points); - assert_eq!(expected_de_casteljau_points[3][0], bezier.evaluate(ComputeType::Parametric(0.5))); + assert_eq!(expected_de_casteljau_points[3][0], bezier.evaluate(TValue::Parametric(0.5))); } #[test] @@ -433,16 +434,16 @@ mod tests { let linear = Bezier::from_linear_dvec2(p1, p2); let unit_slope = DVec2::new(30., 20.).normalize(); - assert_eq!(linear.tangent(0.), unit_slope); - assert_eq!(linear.tangent(1.), unit_slope); + assert_eq!(linear.tangent(TValue::Parametric(0.)), unit_slope); + assert_eq!(linear.tangent(TValue::Parametric(1.)), unit_slope); let quadratic = Bezier::from_quadratic_dvec2(p1, p2, p3); - assert_eq!(quadratic.tangent(0.), DVec2::new(60., 40.).normalize()); - assert_eq!(quadratic.tangent(1.), DVec2::new(40., 60.).normalize()); + assert_eq!(quadratic.tangent(TValue::Parametric(0.)), DVec2::new(60., 40.).normalize()); + assert_eq!(quadratic.tangent(TValue::Parametric(1.)), DVec2::new(40., 60.).normalize()); let cubic = Bezier::from_cubic_dvec2(p1, p2, p3, p4); - assert_eq!(cubic.tangent(0.), DVec2::new(90., 60.).normalize()); - assert_eq!(cubic.tangent(1.), DVec2::new(30., 120.).normalize()); + assert_eq!(cubic.tangent(TValue::Parametric(0.)), DVec2::new(90., 60.).normalize()); + assert_eq!(cubic.tangent(TValue::Parametric(1.)), DVec2::new(30., 120.).normalize()); } #[test] @@ -455,16 +456,16 @@ mod tests { let linear = Bezier::from_linear_dvec2(p1, p2); let unit_slope = DVec2::new(-20., 30.).normalize(); - assert_eq!(linear.normal(0.), unit_slope); - assert_eq!(linear.normal(1.), unit_slope); + assert_eq!(linear.normal(TValue::Parametric(0.)), unit_slope); + assert_eq!(linear.normal(TValue::Parametric(1.)), unit_slope); let quadratic = Bezier::from_quadratic_dvec2(p1, p2, p3); - assert_eq!(quadratic.normal(0.), DVec2::new(-40., 60.).normalize()); - assert_eq!(quadratic.normal(1.), DVec2::new(-60., 40.).normalize()); + assert_eq!(quadratic.normal(TValue::Parametric(0.)), DVec2::new(-40., 60.).normalize()); + assert_eq!(quadratic.normal(TValue::Parametric(1.)), DVec2::new(-60., 40.).normalize()); let cubic = Bezier::from_cubic_dvec2(p1, p2, p3, p4); - assert_eq!(cubic.normal(0.), DVec2::new(-60., 90.).normalize()); - assert_eq!(cubic.normal(1.), DVec2::new(-120., 30.).normalize()); + assert_eq!(cubic.normal(TValue::Parametric(0.)), DVec2::new(-60., 90.).normalize()); + assert_eq!(cubic.normal(TValue::Parametric(1.)), DVec2::new(-120., 30.).normalize()); } #[test] @@ -475,24 +476,24 @@ mod tests { let p4 = DVec2::new(50., 10.); let linear = Bezier::from_linear_dvec2(p1, p2); - assert_eq!(linear.curvature(0.), 0.); - assert_eq!(linear.curvature(0.5), 0.); - assert_eq!(linear.curvature(1.), 0.); + assert_eq!(linear.curvature(TValue::Parametric(0.)), 0.); + assert_eq!(linear.curvature(TValue::Parametric(0.5)), 0.); + assert_eq!(linear.curvature(TValue::Parametric(1.)), 0.); let quadratic = Bezier::from_quadratic_dvec2(p1, p2, p3); - assert!(compare_f64s(quadratic.curvature(0.), 0.0125)); - assert!(compare_f64s(quadratic.curvature(0.5), 0.035355)); - assert!(compare_f64s(quadratic.curvature(1.), 0.0125)); + assert!(compare_f64s(quadratic.curvature(TValue::Parametric(0.)), 0.0125)); + assert!(compare_f64s(quadratic.curvature(TValue::Parametric(0.5)), 0.035355)); + assert!(compare_f64s(quadratic.curvature(TValue::Parametric(1.)), 0.0125)); let cubic = Bezier::from_cubic_dvec2(p1, p2, p3, p4); - assert!(compare_f64s(cubic.curvature(0.), 0.016667)); - assert!(compare_f64s(cubic.curvature(0.5), 0.)); - assert!(compare_f64s(cubic.curvature(1.), 0.)); + assert!(compare_f64s(cubic.curvature(TValue::Parametric(0.)), 0.016667)); + assert!(compare_f64s(cubic.curvature(TValue::Parametric(0.5)), 0.)); + assert!(compare_f64s(cubic.curvature(TValue::Parametric(1.)), 0.)); // The curvature at an inflection point is zero let inflection_curve = Bezier::from_cubic_coordinates(30., 30., 30., 150., 150., 30., 150., 150.); let inflections = inflection_curve.inflections(); - assert_eq!(inflection_curve.curvature(inflections[0]), 0.); + assert_eq!(inflection_curve.curvature(TValue::Parametric(inflections[0])), 0.); } #[test] @@ -609,12 +610,12 @@ mod tests { let line1 = Bezier::from_linear_coordinates(20., 60., 70., 60.); let intersections1 = bezier.intersections(&line1, None, None); assert!(intersections1.len() == 1); - assert!(compare_points(bezier.evaluate(ComputeType::Parametric(intersections1[0])), DVec2::new(30., 60.))); + assert!(compare_points(bezier.evaluate(TValue::Parametric(intersections1[0])), DVec2::new(30., 60.))); // Intersection in the middle of curve let line2 = Bezier::from_linear_coordinates(150., 150., 30., 30.); let intersections2 = bezier.intersections(&line2, None, None); - assert!(compare_points(bezier.evaluate(ComputeType::Parametric(intersections2[0])), DVec2::new(96., 96.))); + assert!(compare_points(bezier.evaluate(TValue::Parametric(intersections2[0])), DVec2::new(96., 96.))); } #[test] @@ -628,12 +629,12 @@ mod tests { let line1 = Bezier::from_linear_coordinates(20., 50., 40., 50.); let intersections1 = bezier.intersections(&line1, None, None); assert!(intersections1.len() == 1); - assert!(compare_points(bezier.evaluate(ComputeType::Parametric(intersections1[0])), p1)); + assert!(compare_points(bezier.evaluate(TValue::Parametric(intersections1[0])), p1)); // Intersection in the middle of curve let line2 = Bezier::from_linear_coordinates(150., 150., 30., 30.); let intersections2 = bezier.intersections(&line2, None, None); - assert!(compare_points(bezier.evaluate(ComputeType::Parametric(intersections2[0])), DVec2::new(47.77355, 47.77354))); + assert!(compare_points(bezier.evaluate(TValue::Parametric(intersections2[0])), DVec2::new(47.77355, 47.77354))); } #[test] @@ -648,14 +649,14 @@ mod tests { let line1 = Bezier::from_linear_coordinates(20., 30., 40., 30.); let intersections1 = bezier.intersections(&line1, None, None); assert!(intersections1.len() == 1); - assert!(compare_points(bezier.evaluate(ComputeType::Parametric(intersections1[0])), p1)); + assert!(compare_points(bezier.evaluate(TValue::Parametric(intersections1[0])), p1)); // Intersection at edge and in middle of curve, Discriminant < 0 let line2 = Bezier::from_linear_coordinates(150., 150., 30., 30.); let intersections2 = bezier.intersections(&line2, None, None); assert!(intersections2.len() == 2); - assert!(compare_points(bezier.evaluate(ComputeType::Parametric(intersections2[0])), p1)); - assert!(compare_points(bezier.evaluate(ComputeType::Parametric(intersections2[1])), DVec2::new(85.84, 85.84))); + assert!(compare_points(bezier.evaluate(TValue::Parametric(intersections2[0])), p1)); + assert!(compare_points(bezier.evaluate(TValue::Parametric(intersections2[1])), DVec2::new(85.84, 85.84))); } #[test] @@ -672,7 +673,7 @@ mod tests { let intersections = bezier.intersections(&line, None, None); assert_eq!(intersections.len(), 1); - assert!(compare_points(bezier.evaluate(ComputeType::Parametric(intersections[0])), p4)); + assert!(compare_points(bezier.evaluate(TValue::Parametric(intersections[0])), p4)); } #[test] @@ -699,8 +700,8 @@ mod tests { let intersections1 = bezier1.intersections(&bezier2, None, None); let intersections2 = bezier2.intersections(&bezier1, None, None); - let intersections1_points: Vec = intersections1.iter().map(|&t| bezier1.evaluate(ComputeType::Parametric(t))).collect(); - let intersections2_points: Vec = intersections2.iter().map(|&t| bezier2.evaluate(ComputeType::Parametric(t))).rev().collect(); + let intersections1_points: Vec = intersections1.iter().map(|&t| bezier1.evaluate(TValue::Parametric(t))).collect(); + let intersections2_points: Vec = intersections2.iter().map(|&t| bezier2.evaluate(TValue::Parametric(t))).rev().collect(); assert!(compare_vec_of_points(intersections1_points, intersections2_points, 2.)); } @@ -710,8 +711,8 @@ mod tests { let bezier = Bezier::from_cubic_coordinates(160., 180., 170., 10., 30., 90., 180., 140.); let intersections = bezier.self_intersections(Some(0.5)); assert!(compare_vec_of_points( - intersections.iter().map(|&t| bezier.evaluate(ComputeType::Parametric(t[0]))).collect(), - intersections.iter().map(|&t| bezier.evaluate(ComputeType::Parametric(t[1]))).collect(), + intersections.iter().map(|&t| bezier.evaluate(TValue::Parametric(t[0]))).collect(), + intersections.iter().map(|&t| bezier.evaluate(TValue::Parametric(t[1]))).collect(), 2. )); assert!(Bezier::from_linear_coordinates(160., 180., 170., 10.).self_intersections(None).is_empty()); diff --git a/libraries/bezier-rs/src/bezier/transform.rs b/libraries/bezier-rs/src/bezier/transform.rs index 5cf7d5ea..3e1a5dd3 100644 --- a/libraries/bezier-rs/src/bezier/transform.rs +++ b/libraries/bezier-rs/src/bezier/transform.rs @@ -1,14 +1,15 @@ use super::*; -use crate::utils::{f64_compare, ComputeType}; +use crate::utils::{f64_compare, TValue}; use glam::DMat2; use std::f64::consts::PI; /// Functionality that transform Beziers, such as split, reduce, offset, etc. impl Bezier { - /// Returns the pair of Bezier curves that result from splitting the original curve at the point corresponding to `t`. - pub fn split(&self, t: f64) -> [Bezier; 2] { - let split_point = self.evaluate(ComputeType::Parametric(t)); + /// Returns the pair of Bezier curves that result from splitting the original curve at the point `t` along the curve. + pub fn split(&self, t: TValue) -> [Bezier; 2] { + let t = self.t_value_to_parametric(t); + let split_point = self.evaluate(TValue::Parametric(t)); match self.handles { BezierHandles::Linear => [Bezier::from_linear_dvec2(self.start, split_point), Bezier::from_linear_dvec2(split_point, self.end)], @@ -49,11 +50,13 @@ impl Bezier { } } - /// Returns the Bezier curve representing the sub-curve starting at the point corresponding to `t1` and ending at the point corresponding to `t2`. - pub fn trim(&self, t1: f64, t2: f64) -> Bezier { + /// Returns the Bezier curve representing the sub-curve starting at the point `t1` and ending at the point `t2` along the curve. + /// When `t1 < t2`, returns the reversed sub-curve starting at `t2` and ending at `t1`. + pub fn trim(&self, t1: TValue, t2: TValue) -> Bezier { + let (t1, t2) = (self.t_value_to_parametric(t1), self.t_value_to_parametric(t2)); // If t1 is equal to t2, return a bezier comprised entirely of the same point if f64_compare(t1, t2, MAX_ABSOLUTE_DIFFERENCE) { - let point = self.evaluate(ComputeType::Parametric(t1)); + let point = self.evaluate(TValue::Parametric(t1)); return match self.handles { BezierHandles::Linear => Bezier::from_linear_dvec2(point, point), BezierHandles::Quadratic { handle: _ } => Bezier::from_quadratic_dvec2(point, point, point), @@ -63,7 +66,7 @@ impl Bezier { // Depending on the order of `t1` and `t2`, determine which half of the split we need to keep let t1_split_side = usize::from(t1 <= t2); let t2_split_side = usize::from(t1 > t2); - let bezier_starting_at_t1 = self.split(t1)[t1_split_side]; + let bezier_starting_at_t1 = self.split(TValue::Parametric(t1))[t1_split_side]; // Adjust the ratio `t2` to its corresponding value on the new curve that was split on `t1` let adjusted_t2 = if t1 < t2 || t1 == 0. { // Case where we took the split from t1 to the end @@ -73,7 +76,7 @@ impl Bezier { // Case where we took the split from the beginning to `t1` t2 / t1 }; - let result = bezier_starting_at_t1.split(adjusted_t2)[t2_split_side]; + let result = bezier_starting_at_t1.split(TValue::Parametric(adjusted_t2))[t2_split_side]; if t2 < t1 { return result.reverse(); } @@ -132,8 +135,8 @@ impl Bezier { } } // Verify the angle formed by the endpoint normals is sufficiently small, ensuring the on-curve point for `t = 0.5` occurs roughly in the center of the polygon. - let normal_0 = self.normal(0.); - let normal_1 = self.normal(1.); + let normal_0 = self.normal(TValue::Parametric(0.)); + let normal_1 = self.normal(TValue::Parametric(1.)); let endpoint_normal_angle = (normal_0.x * normal_1.x + normal_0.y * normal_1.y).acos(); endpoint_normal_angle < SCALABLE_CURVE_MAX_ENDPOINT_NORMAL_ANGLE } @@ -169,7 +172,7 @@ impl Bezier { extrema.windows(2).for_each(|t_pair| { let t_subcurve_start = t_pair[0]; let t_subcurve_end = t_pair[1]; - let subcurve = self.trim(t_subcurve_start, t_subcurve_end); + let subcurve = self.trim(TValue::Parametric(t_subcurve_start), TValue::Parametric(t_subcurve_end)); // Perform no processing on the subcurve if it's already scalable. if subcurve.is_scalable() { result_beziers.push(subcurve); @@ -177,7 +180,7 @@ impl Bezier { return; } // According to , it is generally sufficient to split subcurves with no local extrema at `t = 0.5` to generate two scalable segments. - let [first_half, second_half] = subcurve.split(0.5); + let [first_half, second_half] = subcurve.split(TValue::Parametric(0.5)); if first_half.is_scalable() && second_half.is_scalable() { result_beziers.push(first_half); result_beziers.push(second_half); @@ -191,14 +194,14 @@ impl Bezier { let mut t1 = 0.; let mut t2 = step_size; while t2 <= 1. + step_size { - segment = subcurve.trim(t1, f64::min(t2, 1.)); + segment = subcurve.trim(TValue::Parametric(t1), TValue::Parametric(f64::min(t2, 1.))); if !segment.is_scalable() { t2 -= step_size; // If the previous step does not exist, the start of the subcurve is irreducible. // Otherwise, add the valid segment from the previous step to the result. if f64::abs(t1 - t2) >= step_size { - segment = subcurve.trim(t1, t2); + segment = subcurve.trim(TValue::Parametric(t1), TValue::Parametric(t2)); result_beziers.push(segment); result_t_values.push(t_subcurve_start + t2 * (t_subcurve_end - t_subcurve_start)); } else { @@ -210,7 +213,7 @@ impl Bezier { } // Collect final remainder of the curve. if t1 < 1. { - segment = subcurve.trim(t1, 1.); + segment = subcurve.trim(TValue::Parametric(t1), TValue::Parametric(1.)); if segment.is_scalable() { result_beziers.push(segment); result_t_values.push(t_subcurve_end); @@ -236,8 +239,8 @@ impl Bezier { fn scale(&self, distance: f64) -> Bezier { assert!(self.is_scalable(), "The curve provided to scale is not scalable. Reduce the curve first."); - let normal_start = self.normal(0.); - let normal_end = self.normal(1.); + let normal_start = self.normal(TValue::Parametric(0.)); + let normal_end = self.normal(TValue::Parametric(1.)); // If normal unit vectors are equal, then the lines are parallel if normal_start.abs_diff_eq(normal_end, MAX_ABSOLUTE_DIFFERENCE) { @@ -263,30 +266,30 @@ impl Bezier { pub fn graduated_scale(&self, start_distance: f64, end_distance: f64) -> Bezier { assert!(self.is_scalable(), "The curve provided to scale is not scalable. Reduce the curve first."); - let normal_start = self.normal(0.); - let normal_end = self.normal(1.); + let normal_start = self.normal(TValue::Parametric(0.)); + let normal_end = self.normal(TValue::Parametric(1.)); // If normal unit vectors are equal, then the lines are parallel if normal_start.abs_diff_eq(normal_end, MAX_ABSOLUTE_DIFFERENCE) { - let transformed_start = utils::scale_point_from_direction_vector(self.start, self.normal(0.), false, start_distance); - let transformed_end = utils::scale_point_from_direction_vector(self.end, self.normal(1.), false, end_distance); + let transformed_start = utils::scale_point_from_direction_vector(self.start, self.normal(TValue::Parametric(0.)), false, start_distance); + let transformed_end = utils::scale_point_from_direction_vector(self.end, self.normal(TValue::Parametric(1.)), false, end_distance); return match self.handles { BezierHandles::Linear => Bezier::from_linear_dvec2(transformed_start, transformed_end), BezierHandles::Quadratic { handle } => { let handle_closest_t = self.project(handle, ProjectionOptions::default()); let handle_scale_distance = (1. - handle_closest_t) * start_distance + handle_closest_t * end_distance; - let transformed_handle = utils::scale_point_from_direction_vector(handle, self.normal(handle_closest_t), false, handle_scale_distance); + let transformed_handle = utils::scale_point_from_direction_vector(handle, self.normal(TValue::Parametric(handle_closest_t)), false, handle_scale_distance); Bezier::from_quadratic_dvec2(transformed_start, transformed_handle, transformed_end) } BezierHandles::Cubic { handle_start, handle_end } => { let handle_start_closest_t = self.project(handle_start, ProjectionOptions::default()); let handle_start_scale_distance = (1. - handle_start_closest_t) * start_distance + handle_start_closest_t * end_distance; - let transformed_handle_start = utils::scale_point_from_direction_vector(handle_start, self.normal(handle_start_closest_t), false, handle_start_scale_distance); + let transformed_handle_start = utils::scale_point_from_direction_vector(handle_start, self.normal(TValue::Parametric(handle_start_closest_t)), false, handle_start_scale_distance); let handle_end_closest_t = self.project(handle_start, ProjectionOptions::default()); let handle_end_scale_distance = (1. - handle_end_closest_t) * start_distance + handle_end_closest_t * end_distance; - let transformed_handle_end = utils::scale_point_from_direction_vector(handle_end, self.normal(handle_end_closest_t), false, handle_end_scale_distance); + let transformed_handle_end = utils::scale_point_from_direction_vector(handle_end, self.normal(TValue::Parametric(handle_end_closest_t)), false, handle_end_scale_distance); Bezier::from_cubic_dvec2(transformed_start, transformed_handle_start, transformed_handle_end, transformed_end) } }; @@ -399,7 +402,7 @@ impl Bezier { match maximize_arcs { ArcStrategy::Automatic => { let (auto_arcs, final_low_t) = self.approximate_curve_with_arcs(0., 1., error, max_iterations, true); - let arc_approximations = self.split(final_low_t)[1].arcs(ArcsOptions { + let arc_approximations = self.split(TValue::Parametric(final_low_t))[1].arcs(ArcsOptions { strategy: ArcStrategy::FavorCorrectness, error, max_iterations, @@ -444,9 +447,9 @@ impl Bezier { // Inner loop to find the next maximal segment of the curve that can be approximated with a circular arc while iterations <= max_iterations { iterations += 1; - let p1 = self.evaluate(ComputeType::Parametric(low)); - let p2 = self.evaluate(ComputeType::Parametric(middle)); - let p3 = self.evaluate(ComputeType::Parametric(high)); + let p1 = self.evaluate(TValue::Parametric(low)); + let p2 = self.evaluate(TValue::Parametric(middle)); + let p3 = self.evaluate(TValue::Parametric(high)); let wrapped_center = utils::compute_circle_center_from_points(p1, p2, p3); // If the segment is linear, move on to next segment @@ -486,8 +489,8 @@ impl Bezier { }; // Use points in between low, middle, and high to evaluate how well the arc approximates the curve - let e1 = self.evaluate(ComputeType::Parametric((low + middle) / 2.)); - let e2 = self.evaluate(ComputeType::Parametric((middle + high) / 2.)); + let e1 = self.evaluate(TValue::Parametric((low + middle) / 2.)); + let e2 = self.evaluate(TValue::Parametric((middle + high) / 2.)); // Iterate until we find the largest good approximation such that the next iteration is not a good approximation with an arc if utils::f64_compare(radius, e1.distance(center), error) && utils::f64_compare(radius, e2.distance(center), error) { @@ -537,7 +540,7 @@ impl Bezier { #[cfg(test)] mod tests { - use crate::utils::ComputeType; + use crate::utils::TValue; use super::compare::{compare_arcs, compare_vector_of_beziers}; use super::*; @@ -545,37 +548,37 @@ mod tests { #[test] fn test_split() { let line = Bezier::from_linear_coordinates(25., 25., 75., 75.); - let [part1, part2] = line.split(0.5); + let [part1, part2] = line.split(TValue::Parametric(0.5)); assert_eq!(part1.start(), line.start()); - assert_eq!(part1.end(), line.evaluate(ComputeType::Parametric(0.5))); - assert_eq!(part1.evaluate(ComputeType::Parametric(0.5)), line.evaluate(ComputeType::Parametric(0.25))); + assert_eq!(part1.end(), line.evaluate(TValue::Parametric(0.5))); + assert_eq!(part1.evaluate(TValue::Parametric(0.5)), line.evaluate(TValue::Parametric(0.25))); - assert_eq!(part2.start(), line.evaluate(ComputeType::Parametric(0.5))); + assert_eq!(part2.start(), line.evaluate(TValue::Parametric(0.5))); assert_eq!(part2.end(), line.end()); - assert_eq!(part2.evaluate(ComputeType::Parametric(0.5)), line.evaluate(ComputeType::Parametric(0.75))); + assert_eq!(part2.evaluate(TValue::Parametric(0.5)), line.evaluate(TValue::Parametric(0.75))); let quad_bezier = Bezier::from_quadratic_coordinates(10., 10., 50., 50., 90., 10.); - let [part3, part4] = quad_bezier.split(0.5); + let [part3, part4] = quad_bezier.split(TValue::Parametric(0.5)); assert_eq!(part3.start(), quad_bezier.start()); - assert_eq!(part3.end(), quad_bezier.evaluate(ComputeType::Parametric(0.5))); - assert_eq!(part3.evaluate(ComputeType::Parametric(0.5)), quad_bezier.evaluate(ComputeType::Parametric(0.25))); + assert_eq!(part3.end(), quad_bezier.evaluate(TValue::Parametric(0.5))); + assert_eq!(part3.evaluate(TValue::Parametric(0.5)), quad_bezier.evaluate(TValue::Parametric(0.25))); - assert_eq!(part4.start(), quad_bezier.evaluate(ComputeType::Parametric(0.5))); + assert_eq!(part4.start(), quad_bezier.evaluate(TValue::Parametric(0.5))); assert_eq!(part4.end(), quad_bezier.end()); - assert_eq!(part4.evaluate(ComputeType::Parametric(0.5)), quad_bezier.evaluate(ComputeType::Parametric(0.75))); + assert_eq!(part4.evaluate(TValue::Parametric(0.5)), quad_bezier.evaluate(TValue::Parametric(0.75))); let cubic_bezier = Bezier::from_cubic_coordinates(10., 10., 50., 50., 90., 10., 40., 50.); - let [part5, part6] = cubic_bezier.split(0.5); + let [part5, part6] = cubic_bezier.split(TValue::Parametric(0.5)); assert_eq!(part5.start(), cubic_bezier.start()); - assert_eq!(part5.end(), cubic_bezier.evaluate(ComputeType::Parametric(0.5))); - assert_eq!(part5.evaluate(ComputeType::Parametric(0.5)), cubic_bezier.evaluate(ComputeType::Parametric(0.25))); + assert_eq!(part5.end(), cubic_bezier.evaluate(TValue::Parametric(0.5))); + assert_eq!(part5.evaluate(TValue::Parametric(0.5)), cubic_bezier.evaluate(TValue::Parametric(0.25))); - assert_eq!(part6.start(), cubic_bezier.evaluate(ComputeType::Parametric(0.5))); + assert_eq!(part6.start(), cubic_bezier.evaluate(TValue::Parametric(0.5))); assert_eq!(part6.end(), cubic_bezier.end()); - assert_eq!(part6.evaluate(ComputeType::Parametric(0.5)), cubic_bezier.evaluate(ComputeType::Parametric(0.75))); + assert_eq!(part6.evaluate(TValue::Parametric(0.5)), cubic_bezier.evaluate(TValue::Parametric(0.75))); } #[test] @@ -586,24 +589,24 @@ mod tests { let bezier_quadratic = Bezier::from_quadratic_dvec2(start, DVec2::new(140., 30.), end); // Test splitting a quadratic bezier at the startpoint - let [point_bezier1, remainder1] = bezier_quadratic.split(0.); + let [point_bezier1, remainder1] = bezier_quadratic.split(TValue::Parametric(0.)); assert_eq!(point_bezier1, Bezier::from_quadratic_dvec2(start, start, start)); assert!(remainder1.abs_diff_eq(&bezier_quadratic, MAX_ABSOLUTE_DIFFERENCE)); // Test splitting a quadratic bezier at the endpoint - let [remainder2, point_bezier2] = bezier_quadratic.split(1.); + let [remainder2, point_bezier2] = bezier_quadratic.split(TValue::Parametric(1.)); assert_eq!(point_bezier2, Bezier::from_quadratic_dvec2(end, end, end)); assert!(remainder2.abs_diff_eq(&bezier_quadratic, MAX_ABSOLUTE_DIFFERENCE)); let bezier_cubic = Bezier::from_cubic_dvec2(start, DVec2::new(60., 140.), DVec2::new(150., 30.), end); // Test splitting a cubic bezier at the startpoint - let [point_bezier3, remainder3] = bezier_cubic.split(0.); + let [point_bezier3, remainder3] = bezier_cubic.split(TValue::Parametric(0.)); assert_eq!(point_bezier3, Bezier::from_cubic_dvec2(start, start, start, start)); assert!(remainder3.abs_diff_eq(&bezier_cubic, MAX_ABSOLUTE_DIFFERENCE)); // Test splitting a cubic bezier at the endpoint - let [remainder4, point_bezier4] = bezier_cubic.split(1.); + let [remainder4, point_bezier4] = bezier_cubic.split(TValue::Parametric(1.)); assert_eq!(point_bezier4, Bezier::from_cubic_dvec2(end, end, end, end)); assert!(remainder4.abs_diff_eq(&bezier_cubic, MAX_ABSOLUTE_DIFFERENCE)); } @@ -611,39 +614,39 @@ mod tests { #[test] fn test_trim() { let line = Bezier::from_linear_coordinates(80., 80., 40., 40.); - let trimmed1 = line.trim(0.25, 0.75); + let trimmed1 = line.trim(TValue::Parametric(0.25), TValue::Parametric(0.75)); - assert_eq!(trimmed1.start(), line.evaluate(ComputeType::Parametric(0.25))); - assert_eq!(trimmed1.end(), line.evaluate(ComputeType::Parametric(0.75))); - assert_eq!(trimmed1.evaluate(ComputeType::Parametric(0.5)), line.evaluate(ComputeType::Parametric(0.5))); + assert_eq!(trimmed1.start(), line.evaluate(TValue::Parametric(0.25))); + assert_eq!(trimmed1.end(), line.evaluate(TValue::Parametric(0.75))); + assert_eq!(trimmed1.evaluate(TValue::Parametric(0.5)), line.evaluate(TValue::Parametric(0.5))); let quadratic_bezier = Bezier::from_quadratic_coordinates(80., 80., 40., 40., 70., 70.); - let trimmed2 = quadratic_bezier.trim(0.25, 0.75); + let trimmed2 = quadratic_bezier.trim(TValue::Parametric(0.25), TValue::Parametric(0.75)); - assert_eq!(trimmed2.start(), quadratic_bezier.evaluate(ComputeType::Parametric(0.25))); - assert_eq!(trimmed2.end(), quadratic_bezier.evaluate(ComputeType::Parametric(0.75))); - assert_eq!(trimmed2.evaluate(ComputeType::Parametric(0.5)), quadratic_bezier.evaluate(ComputeType::Parametric(0.5))); + assert_eq!(trimmed2.start(), quadratic_bezier.evaluate(TValue::Parametric(0.25))); + assert_eq!(trimmed2.end(), quadratic_bezier.evaluate(TValue::Parametric(0.75))); + assert_eq!(trimmed2.evaluate(TValue::Parametric(0.5)), quadratic_bezier.evaluate(TValue::Parametric(0.5))); let cubic_bezier = Bezier::from_cubic_coordinates(80., 80., 40., 40., 70., 70., 150., 150.); - let trimmed3 = cubic_bezier.trim(0.25, 0.75); + let trimmed3 = cubic_bezier.trim(TValue::Parametric(0.25), TValue::Parametric(0.75)); - assert_eq!(trimmed3.start(), cubic_bezier.evaluate(ComputeType::Parametric(0.25))); - assert_eq!(trimmed3.end(), cubic_bezier.evaluate(ComputeType::Parametric(0.75))); - assert_eq!(trimmed3.evaluate(ComputeType::Parametric(0.5)), cubic_bezier.evaluate(ComputeType::Parametric(0.5))); + assert_eq!(trimmed3.start(), cubic_bezier.evaluate(TValue::Parametric(0.25))); + assert_eq!(trimmed3.end(), cubic_bezier.evaluate(TValue::Parametric(0.75))); + assert_eq!(trimmed3.evaluate(TValue::Parametric(0.5)), cubic_bezier.evaluate(TValue::Parametric(0.5))); } #[test] fn test_trim_t2_greater_than_t1() { // Test trimming quadratic curve when t2 > t1 let bezier_quadratic = Bezier::from_quadratic_coordinates(30., 50., 140., 30., 160., 170.); - let trim1 = bezier_quadratic.trim(0.25, 0.75); - let trim2 = bezier_quadratic.trim(0.75, 0.25).reverse(); + let trim1 = bezier_quadratic.trim(TValue::Parametric(0.25), TValue::Parametric(0.75)); + let trim2 = bezier_quadratic.trim(TValue::Parametric(0.75), TValue::Parametric(0.25)).reverse(); assert!(trim1.abs_diff_eq(&trim2, MAX_ABSOLUTE_DIFFERENCE)); // Test trimming cubic curve when t2 > t1 let bezier_cubic = Bezier::from_cubic_coordinates(30., 30., 60., 140., 150., 30., 160., 160.); - let trim3 = bezier_cubic.trim(0.25, 0.75); - let trim4 = bezier_cubic.trim(0.75, 0.25).reverse(); + let trim3 = bezier_cubic.trim(TValue::Parametric(0.25), TValue::Parametric(0.75)); + let trim4 = bezier_cubic.trim(TValue::Parametric(0.75), TValue::Parametric(0.25)).reverse(); assert!(trim3.abs_diff_eq(&trim4, MAX_ABSOLUTE_DIFFERENCE)); } @@ -704,7 +707,7 @@ mod tests { assert!(reduced_curves .iter() .zip(helper_t_values.windows(2)) - .all(|(curve, t_pair)| curve.abs_diff_eq(&bezier.trim(t_pair[0], t_pair[1]), MAX_ABSOLUTE_DIFFERENCE))) + .all(|(curve, t_pair)| curve.abs_diff_eq(&bezier.trim(TValue::Parametric(t_pair[0]), TValue::Parametric(t_pair[1])), MAX_ABSOLUTE_DIFFERENCE))) } #[test] @@ -744,23 +747,23 @@ mod tests { // Assert the first length-wise piece of the outline is 10 units from the line assert!(f64_compare( - outline[0].evaluate(ComputeType::Parametric(0.25)).distance(line.evaluate(ComputeType::Parametric(0.25))), + outline[0].evaluate(TValue::Parametric(0.25)).distance(line.evaluate(TValue::Parametric(0.25))), 10., MAX_ABSOLUTE_DIFFERENCE )); // f64 // Assert the first cap touches the line end point at the halfway point - assert!(outline[1].evaluate(ComputeType::Parametric(0.5)).abs_diff_eq(line.end(), MAX_ABSOLUTE_DIFFERENCE)); + assert!(outline[1].evaluate(TValue::Parametric(0.5)).abs_diff_eq(line.end(), MAX_ABSOLUTE_DIFFERENCE)); // Assert the second length-wise piece of the outline is 10 units from the line assert!(f64_compare( - outline[2].evaluate(ComputeType::Parametric(0.25)).distance(line.evaluate(ComputeType::Parametric(0.75))), + outline[2].evaluate(TValue::Parametric(0.25)).distance(line.evaluate(TValue::Parametric(0.75))), 10., MAX_ABSOLUTE_DIFFERENCE )); // f64 // Assert the second cap touches the line start point at the halfway point - assert!(outline[3].evaluate(ComputeType::Parametric(0.5)).abs_diff_eq(line.start(), MAX_ABSOLUTE_DIFFERENCE)); + assert!(outline[3].evaluate(TValue::Parametric(0.5)).abs_diff_eq(line.start(), MAX_ABSOLUTE_DIFFERENCE)); } #[test] @@ -778,17 +781,17 @@ mod tests { // Assert the scaled bezier is 30 units from the line assert!(f64_compare( - scaled_bezier.evaluate(ComputeType::Parametric(0.)).distance(bezier.evaluate(ComputeType::Parametric(0.))), + scaled_bezier.evaluate(TValue::Parametric(0.)).distance(bezier.evaluate(TValue::Parametric(0.))), 30., MAX_ABSOLUTE_DIFFERENCE )); assert!(f64_compare( - scaled_bezier.evaluate(ComputeType::Parametric(1.)).distance(bezier.evaluate(ComputeType::Parametric(1.))), + scaled_bezier.evaluate(TValue::Parametric(1.)).distance(bezier.evaluate(TValue::Parametric(1.))), 30., MAX_ABSOLUTE_DIFFERENCE )); assert!(f64_compare( - scaled_bezier.evaluate(ComputeType::Parametric(0.5)).distance(bezier.evaluate(ComputeType::Parametric(0.5))), + scaled_bezier.evaluate(TValue::Parametric(0.5)).distance(bezier.evaluate(TValue::Parametric(0.5))), 30., MAX_ABSOLUTE_DIFFERENCE )); diff --git a/libraries/bezier-rs/src/consts.rs b/libraries/bezier-rs/src/consts.rs index 55ce5adb..5bdec44e 100644 --- a/libraries/bezier-rs/src/consts.rs +++ b/libraries/bezier-rs/src/consts.rs @@ -10,6 +10,8 @@ pub const NUM_DISTANCES: usize = 5; pub const SCALABLE_CURVE_MAX_ENDPOINT_NORMAL_ANGLE: f64 = std::f64::consts::PI / 3.; /// Minimum allowable separation between adjacent `t` values when calculating curve intersections pub const MIN_SEPERATION_VALUE: f64 = 5. * 1e-3; +/// Default error bound for `t_value_to_parametric` function when TValue argument is Euclidean +pub const DEFAULT_EUCLIDEAN_ERROR_BOUND: f64 = 0.001; // Method argument defaults diff --git a/libraries/bezier-rs/src/lib.rs b/libraries/bezier-rs/src/lib.rs index 0da47242..e12545c5 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::ComputeType; +pub use utils::TValue; diff --git a/libraries/bezier-rs/src/subpath/lookup.rs b/libraries/bezier-rs/src/subpath/lookup.rs index f70d6dfa..2e0a3e79 100644 --- a/libraries/bezier-rs/src/subpath/lookup.rs +++ b/libraries/bezier-rs/src/subpath/lookup.rs @@ -1,5 +1,5 @@ use super::*; -use crate::{ComputeType, ProjectionOptions}; +use crate::{ProjectionOptions, TValue}; use glam::DVec2; /// Functionality relating to looking up properties of the `Subpath` or points along the `Subpath`. @@ -22,7 +22,7 @@ impl Subpath { .iter() .map(|bezier| { let project_t = bezier.project(point, options); - (bezier.evaluate(ComputeType::Parametric(project_t)).distance(point), project_t) + (bezier.evaluate(TValue::Parametric(project_t)).distance(point), project_t) }) .enumerate() .min_by(|(_, (distance1, _)), (_, (distance2, _))| distance1.total_cmp(distance2)) diff --git a/libraries/bezier-rs/src/subpath/manipulators.rs b/libraries/bezier-rs/src/subpath/manipulators.rs index 5d4230f0..d781ce8e 100644 --- a/libraries/bezier-rs/src/subpath/manipulators.rs +++ b/libraries/bezier-rs/src/subpath/manipulators.rs @@ -1,14 +1,14 @@ use super::*; use crate::consts::MAX_ABSOLUTE_DIFFERENCE; use crate::utils::f64_compare; -use crate::ComputeType; +use crate::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: ComputeType) { + pub fn insert(&mut self, t: TValue) { match t { - ComputeType::Parametric(t) => { + TValue::Parametric(t) => { assert!((0.0..=1.).contains(&t)); let number_of_curves = self.len_segments() as f64; @@ -25,7 +25,7 @@ impl Subpath { // 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(target_curve_t); + let [first, second] = curve.split(TValue::Parametric(target_curve_t)); let new_group = ManipulatorGroup { anchor: first.end(), in_handle: first.handle_end(), @@ -37,8 +37,8 @@ impl Subpath { self.manipulator_groups[((target_curve_index as usize) + 2) % number_of_groups].in_handle = second.handle_end(); } // TODO: change this implementation to Euclidean compute - ComputeType::Euclidean(_t) => {} - ComputeType::EuclideanWithinError { t: _, epsilon: _ } => todo!(), + TValue::Euclidean(_t) => {} + TValue::EuclideanWithinError { t: _, error: _ } => todo!(), } } } @@ -94,9 +94,9 @@ mod tests { #[test] fn insert_in_first_segment_of_open_subpath() { let mut subpath = set_up_open_subpath(); - let location = subpath.evaluate(ComputeType::Parametric(0.2)); - let split_pair = subpath.iter().next().unwrap().split((0.2 * 3.) % 1.); - subpath.insert(ComputeType::Parametric(0.2)); + let location = subpath.evaluate(TValue::Parametric(0.2)); + let split_pair = subpath.iter().next().unwrap().split(TValue::Parametric((0.2 * 3.) % 1.)); + subpath.insert(TValue::Parametric(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 +105,9 @@ mod tests { #[test] fn insert_in_last_segment_of_open_subpath() { let mut subpath = set_up_open_subpath(); - let location = subpath.evaluate(ComputeType::Parametric(0.9)); - let split_pair = subpath.iter().nth(2).unwrap().split((0.9 * 3.) % 1.); - subpath.insert(ComputeType::Parametric(0.9)); + let location = subpath.evaluate(TValue::Parametric(0.9)); + let split_pair = subpath.iter().nth(2).unwrap().split(TValue::Parametric((0.9 * 3.) % 1.)); + subpath.insert(TValue::Parametric(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 +117,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(ComputeType::Parametric(0.75)); - subpath.insert(ComputeType::Parametric(0.75)); + let location = subpath.evaluate(TValue::Parametric(0.75)); + subpath.insert(TValue::Parametric(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 +127,9 @@ mod tests { #[test] fn insert_at_last_segment_of_closed_subpath() { let mut subpath = set_up_closed_subpath(); - let location = subpath.evaluate(ComputeType::Parametric(0.9)); - let split_pair = subpath.iter().nth(3).unwrap().split((0.9 * 4.) % 1.); - subpath.insert(ComputeType::Parametric(0.9)); + let location = subpath.evaluate(TValue::Parametric(0.9)); + let split_pair = subpath.iter().nth(3).unwrap().split(TValue::Parametric((0.9 * 4.) % 1.)); + subpath.insert(TValue::Parametric(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 +140,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(ComputeType::Parametric(1.)); - subpath.insert(ComputeType::Parametric(1.)); + let location = subpath.evaluate(TValue::Parametric(1.)); + subpath.insert(TValue::Parametric(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 e00f99cb..5dc67571 100644 --- a/libraries/bezier-rs/src/subpath/solvers.rs +++ b/libraries/bezier-rs/src/subpath/solvers.rs @@ -1,26 +1,26 @@ use super::*; use crate::consts::MIN_SEPERATION_VALUE; -use crate::ComputeType; +use crate::TValue; 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: ComputeType) -> DVec2 { + pub fn evaluate(&self, t: TValue) -> DVec2 { match t { - ComputeType::Parametric(t) => { + TValue::Parametric(t) => { assert!((0.0..=1.).contains(&t)); if let (Some(curve), target_curve_t) = self.find_curve_parametric(t) { - curve.evaluate(ComputeType::Parametric(target_curve_t)) + curve.evaluate(TValue::Parametric(target_curve_t)) } else { - self.iter().last().unwrap().evaluate(ComputeType::Parametric(1.)) + self.iter().last().unwrap().evaluate(TValue::Parametric(1.)) } } // TODO: change this implementation to Euclidean compute - ComputeType::Euclidean(_t) => self.iter().next().unwrap().evaluate(ComputeType::Parametric(0.)), - ComputeType::EuclideanWithinError { t: _, epsilon: _ } => todo!(), + TValue::Euclidean(_t) => self.iter().next().unwrap().evaluate(TValue::Parametric(0.)), + TValue::EuclideanWithinError { t: _, error: _ } => todo!(), } } @@ -54,35 +54,35 @@ impl Subpath { intersection_t_values } - pub fn tangent(&self, t: ComputeType) -> DVec2 { + pub fn tangent(&self, t: TValue) -> DVec2 { match t { - ComputeType::Parametric(t) => { + TValue::Parametric(t) => { assert!((0.0..=1.).contains(&t)); if let (Some(curve), target_curve_t) = self.find_curve_parametric(t) { - curve.tangent(target_curve_t) + curve.tangent(TValue::Parametric(target_curve_t)) } else { - self.iter().last().unwrap().tangent(1.) + self.iter().last().unwrap().tangent(TValue::Parametric(1.)) } } - ComputeType::Euclidean(_t) => unimplemented!(), - ComputeType::EuclideanWithinError { t: _, epsilon: _ } => todo!(), + TValue::Euclidean(_t) => unimplemented!(), + TValue::EuclideanWithinError { t: _, error: _ } => todo!(), } } - pub fn normal(&self, t: ComputeType) -> DVec2 { + pub fn normal(&self, t: TValue) -> DVec2 { match t { - ComputeType::Parametric(t) => { + TValue::Parametric(t) => { assert!((0.0..=1.).contains(&t)); if let (Some(curve), target_curve_t) = self.find_curve_parametric(t) { - curve.normal(target_curve_t) + curve.normal(TValue::Parametric(target_curve_t)) } else { - self.iter().last().unwrap().normal(1.) + self.iter().last().unwrap().normal(TValue::Parametric(1.)) } } - ComputeType::Euclidean(_t) => unimplemented!(), - ComputeType::EuclideanWithinError { t: _, epsilon: _ } => todo!(), + TValue::Euclidean(_t) => unimplemented!(), + TValue::EuclideanWithinError { t: _, error: _ } => todo!(), } } } @@ -124,16 +124,16 @@ mod tests { ); let t0 = 0.; - assert_eq!(subpath.evaluate(ComputeType::Parametric(t0)), bezier.evaluate(ComputeType::Parametric(t0))); + assert_eq!(subpath.evaluate(TValue::Parametric(t0)), bezier.evaluate(TValue::Parametric(t0))); let t1 = 0.25; - assert_eq!(subpath.evaluate(ComputeType::Parametric(t1)), bezier.evaluate(ComputeType::Parametric(t1))); + assert_eq!(subpath.evaluate(TValue::Parametric(t1)), bezier.evaluate(TValue::Parametric(t1))); let t2 = 0.50; - assert_eq!(subpath.evaluate(ComputeType::Parametric(t2)), bezier.evaluate(ComputeType::Parametric(t2))); + assert_eq!(subpath.evaluate(TValue::Parametric(t2)), bezier.evaluate(TValue::Parametric(t2))); let t3 = 1.; - assert_eq!(subpath.evaluate(ComputeType::Parametric(t3)), bezier.evaluate(ComputeType::Parametric(t3))); + assert_eq!(subpath.evaluate(TValue::Parametric(t3)), bezier.evaluate(TValue::Parametric(t3))); } #[test] @@ -176,43 +176,38 @@ mod tests { let t0 = 0.; assert!(utils::dvec2_compare( - subpath.evaluate(ComputeType::Parametric(t0)), - linear_bezier.evaluate(ComputeType::Parametric(normalize_t(n, t0))), + subpath.evaluate(TValue::Parametric(t0)), + linear_bezier.evaluate(TValue::Parametric(normalize_t(n, t0))), MAX_ABSOLUTE_DIFFERENCE ) .all()); let t1 = 0.25; assert!(utils::dvec2_compare( - subpath.evaluate(ComputeType::Parametric(t1)), - linear_bezier.evaluate(ComputeType::Parametric(normalize_t(n, t1))), + subpath.evaluate(TValue::Parametric(t1)), + linear_bezier.evaluate(TValue::Parametric(normalize_t(n, t1))), MAX_ABSOLUTE_DIFFERENCE ) .all()); let t2 = 0.50; assert!(utils::dvec2_compare( - subpath.evaluate(ComputeType::Parametric(t2)), - quadratic_bezier.evaluate(ComputeType::Parametric(normalize_t(n, t2))), + subpath.evaluate(TValue::Parametric(t2)), + quadratic_bezier.evaluate(TValue::Parametric(normalize_t(n, t2))), MAX_ABSOLUTE_DIFFERENCE ) .all()); let t3 = 0.75; assert!(utils::dvec2_compare( - subpath.evaluate(ComputeType::Parametric(t3)), - quadratic_bezier.evaluate(ComputeType::Parametric(normalize_t(n, t3))), + subpath.evaluate(TValue::Parametric(t3)), + quadratic_bezier.evaluate(TValue::Parametric(normalize_t(n, t3))), MAX_ABSOLUTE_DIFFERENCE ) .all()); let t4 = 1.0; - assert!(utils::dvec2_compare( - subpath.evaluate(ComputeType::Parametric(t4)), - quadratic_bezier.evaluate(ComputeType::Parametric(1.)), - MAX_ABSOLUTE_DIFFERENCE - ) - .all()); + assert!(utils::dvec2_compare(subpath.evaluate(TValue::Parametric(t4)), quadratic_bezier.evaluate(TValue::Parametric(1.)), MAX_ABSOLUTE_DIFFERENCE).all()); // Test closed subpath @@ -221,19 +216,14 @@ mod tests { let t5 = 2. / 3.; assert!(utils::dvec2_compare( - subpath.evaluate(ComputeType::Parametric(t5)), - cubic_bezier.evaluate(ComputeType::Parametric(normalize_t(n, t5))), + subpath.evaluate(TValue::Parametric(t5)), + cubic_bezier.evaluate(TValue::Parametric(normalize_t(n, t5))), MAX_ABSOLUTE_DIFFERENCE ) .all()); let t6 = 1.; - assert!(utils::dvec2_compare( - subpath.evaluate(ComputeType::Parametric(t6)), - cubic_bezier.evaluate(ComputeType::Parametric(1.)), - MAX_ABSOLUTE_DIFFERENCE - ) - .all()); + assert!(utils::dvec2_compare(subpath.evaluate(TValue::Parametric(t6)), cubic_bezier.evaluate(TValue::Parametric(1.)), MAX_ABSOLUTE_DIFFERENCE).all()); } #[test] @@ -281,22 +271,22 @@ mod tests { let subpath_intersections = subpath.intersections(&line, None, None); assert!(utils::dvec2_compare( - cubic_bezier.evaluate(ComputeType::Parametric(cubic_intersections[0])), - subpath.evaluate(ComputeType::Parametric(subpath_intersections[0])), + cubic_bezier.evaluate(TValue::Parametric(cubic_intersections[0])), + subpath.evaluate(TValue::Parametric(subpath_intersections[0])), MAX_ABSOLUTE_DIFFERENCE ) .all()); assert!(utils::dvec2_compare( - quadratic_bezier_1.evaluate(ComputeType::Parametric(quadratic_1_intersections[0])), - subpath.evaluate(ComputeType::Parametric(subpath_intersections[1])), + quadratic_bezier_1.evaluate(TValue::Parametric(quadratic_1_intersections[0])), + subpath.evaluate(TValue::Parametric(subpath_intersections[1])), MAX_ABSOLUTE_DIFFERENCE ) .all()); assert!(utils::dvec2_compare( - quadratic_bezier_1.evaluate(ComputeType::Parametric(quadratic_1_intersections[1])), - subpath.evaluate(ComputeType::Parametric(subpath_intersections[2])), + quadratic_bezier_1.evaluate(TValue::Parametric(quadratic_1_intersections[1])), + subpath.evaluate(TValue::Parametric(subpath_intersections[2])), MAX_ABSOLUTE_DIFFERENCE ) .all()); @@ -348,15 +338,15 @@ mod tests { let subpath_intersections = subpath.intersections(&line, None, None); assert!(utils::dvec2_compare( - cubic_bezier.evaluate(ComputeType::Parametric(cubic_intersections[0])), - subpath.evaluate(ComputeType::Parametric(subpath_intersections[0])), + cubic_bezier.evaluate(TValue::Parametric(cubic_intersections[0])), + subpath.evaluate(TValue::Parametric(subpath_intersections[0])), MAX_ABSOLUTE_DIFFERENCE ) .all()); assert!(utils::dvec2_compare( - quadratic_bezier_1.evaluate(ComputeType::Parametric(quadratic_1_intersections[0])), - subpath.evaluate(ComputeType::Parametric(subpath_intersections[1])), + quadratic_bezier_1.evaluate(TValue::Parametric(quadratic_1_intersections[0])), + subpath.evaluate(TValue::Parametric(subpath_intersections[1])), MAX_ABSOLUTE_DIFFERENCE ) .all()); @@ -407,22 +397,22 @@ mod tests { let subpath_intersections = subpath.intersections(&line, None, None); assert!(utils::dvec2_compare( - cubic_bezier.evaluate(ComputeType::Parametric(cubic_intersections[0])), - subpath.evaluate(ComputeType::Parametric(subpath_intersections[0])), + cubic_bezier.evaluate(TValue::Parametric(cubic_intersections[0])), + subpath.evaluate(TValue::Parametric(subpath_intersections[0])), MAX_ABSOLUTE_DIFFERENCE ) .all()); assert!(utils::dvec2_compare( - quadratic_bezier_1.evaluate(ComputeType::Parametric(quadratic_1_intersections[0])), - subpath.evaluate(ComputeType::Parametric(subpath_intersections[1])), + quadratic_bezier_1.evaluate(TValue::Parametric(quadratic_1_intersections[0])), + subpath.evaluate(TValue::Parametric(subpath_intersections[1])), MAX_ABSOLUTE_DIFFERENCE ) .all()); assert!(utils::dvec2_compare( - quadratic_bezier_1.evaluate(ComputeType::Parametric(quadratic_1_intersections[1])), - subpath.evaluate(ComputeType::Parametric(subpath_intersections[2])), + quadratic_bezier_1.evaluate(TValue::Parametric(quadratic_1_intersections[1])), + subpath.evaluate(TValue::Parametric(subpath_intersections[2])), MAX_ABSOLUTE_DIFFERENCE ) .all()); diff --git a/libraries/bezier-rs/src/subpath/transform.rs b/libraries/bezier-rs/src/subpath/transform.rs index 70daa36a..4d5ce54f 100644 --- a/libraries/bezier-rs/src/subpath/transform.rs +++ b/libraries/bezier-rs/src/subpath/transform.rs @@ -1,14 +1,14 @@ use super::*; -use crate::ComputeType; +use crate::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: ComputeType) -> (Subpath, Option) { + pub fn split(&self, t: TValue) -> (Subpath, Option) { match t { - ComputeType::Parametric(t) => { + TValue::Parametric(t) => { assert!((0.0..=1.).contains(&t)); let number_of_curves = self.len_segments() as f64; @@ -22,7 +22,7 @@ impl Subpath { 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(if t == 1. { t } else { target_curve_t }); + 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. { @@ -83,8 +83,8 @@ impl Subpath { } } // TODO: change this implementation to Euclidean compute - ComputeType::Euclidean(_t) => todo!(), - ComputeType::EuclideanWithinError { t: _, epsilon: _ } => todo!(), + TValue::Euclidean(_t) => todo!(), + TValue::EuclideanWithinError { t: _, error: _ } => todo!(), } } } @@ -140,9 +140,9 @@ mod tests { #[test] fn split_an_open_subpath() { let subpath = set_up_open_subpath(); - let location = subpath.evaluate(ComputeType::Parametric(0.2)); - let split_pair = subpath.iter().next().unwrap().split((0.2 * 3.) % 1.); - let (first, second) = subpath.split(ComputeType::Parametric(0.2)); + let location = subpath.evaluate(TValue::Parametric(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)); assert!(second.is_some()); let second = second.unwrap(); assert_eq!(first.manipulator_groups[1].anchor, location); @@ -154,9 +154,9 @@ mod tests { #[test] fn split_at_start_of_an_open_subpath() { let subpath = set_up_open_subpath(); - let location = subpath.evaluate(ComputeType::Parametric(0.)); - let split_pair = subpath.iter().next().unwrap().split(0.); - let (first, second) = subpath.split(ComputeType::Parametric(0.)); + let location = subpath.evaluate(TValue::Parametric(0.)); + let split_pair = subpath.iter().next().unwrap().split(TValue::Parametric(0.)); + let (first, second) = subpath.split(TValue::Parametric(0.)); assert!(second.is_some()); let second = second.unwrap(); assert_eq!( @@ -175,9 +175,9 @@ mod tests { #[test] fn split_at_end_of_an_open_subpath() { let subpath = set_up_open_subpath(); - let location = subpath.evaluate(ComputeType::Parametric(1.)); - let split_pair = subpath.iter().last().unwrap().split(1.); - let (first, second) = subpath.split(ComputeType::Parametric(1.)); + let location = subpath.evaluate(TValue::Parametric(1.)); + let split_pair = subpath.iter().last().unwrap().split(TValue::Parametric(1.)); + let (first, second) = subpath.split(TValue::Parametric(1.)); assert!(second.is_some()); let second = second.unwrap(); assert_eq!(first.manipulator_groups[3].anchor, location); @@ -196,9 +196,9 @@ mod tests { #[test] fn split_a_closed_subpath() { let subpath = set_up_closed_subpath(); - let location = subpath.evaluate(ComputeType::Parametric(0.2)); - let split_pair = subpath.iter().next().unwrap().split((0.2 * 4.) % 1.); - let (first, second) = subpath.split(ComputeType::Parametric(0.2)); + let location = subpath.evaluate(TValue::Parametric(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)); assert!(second.is_none()); assert_eq!(first.manipulator_groups[0].anchor, location); assert_eq!(first.manipulator_groups[5].anchor, location); @@ -210,8 +210,8 @@ mod tests { #[test] fn split_at_start_of_a_closed_subpath() { let subpath = set_up_closed_subpath(); - let location = subpath.evaluate(ComputeType::Parametric(0.)); - let (first, second) = subpath.split(ComputeType::Parametric(0.)); + let location = subpath.evaluate(TValue::Parametric(0.)); + let (first, second) = subpath.split(TValue::Parametric(0.)); assert!(second.is_none()); assert_eq!(first.manipulator_groups[0].anchor, location); assert_eq!(first.manipulator_groups[4].anchor, location); @@ -224,8 +224,8 @@ mod tests { #[test] fn split_at_end_of_a_closed_subpath() { let subpath = set_up_closed_subpath(); - let location = subpath.evaluate(ComputeType::Parametric(1.)); - let (first, second) = subpath.split(ComputeType::Parametric(1.)); + let location = subpath.evaluate(TValue::Parametric(1.)); + let (first, second) = subpath.split(TValue::Parametric(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 9caebd4c..f101fee0 100644 --- a/libraries/bezier-rs/src/utils.rs +++ b/libraries/bezier-rs/src/utils.rs @@ -4,10 +4,18 @@ use glam::{BVec2, DMat2, DVec2}; use std::f64::consts::PI; #[derive(Copy, Clone, PartialEq)] -pub enum ComputeType { +/// A structure which can be used to reference a particular point along a `Bezier`. +/// Assuming a 2-dimensional Bezier is represented as a parametric curve defined by components `(x(f(t), y(f(t))))`, this structure defines variants for `f(t)`. +/// - The `Parametric` variant represents the point calculated using the parametric equation of the curve at argument `t`. That is, `f(t) = t`. Speed along the curve's parametric form is not constant. `t` must lie in the range `[0, 1]`. +/// - The `Euclidean` variant represents the point calculated at a distance ratio `t` along the arc length of the curve in the range `[0, 1]`. Speed is constant along the curve's arc length. +/// - E.g. If `d` is the distance from the start point of a `Bezier` to a certain point along the curve, and `l` is the total arc length of the curve, that certain point lies at a distance ratio `t = d / l`. +/// - All `Bezier` functions will implicitly convert a Euclidean [TValue] argument to a parametric `t`-value using binary search, computed within a particular error. That is, a point at distance ratio `t*`, +/// satisfying `|t* - t| <= error`. The default error is `0.001`. Given this requires a lengthier calculation, it is not recommended to use the `Euclidean` or `EuclideanWithinError` variants frequently in computationally intensive tasks. +/// - The `EuclideanWithinError` variant functions exactly as the `Euclidean` variant, but allows the `error` to be customized when computing `t` internally. +pub enum TValue { Parametric(f64), Euclidean(f64), - EuclideanWithinError { t: f64, epsilon: f64 }, + EuclideanWithinError { t: f64, error: f64 }, } /// Helper to perform the computation of a and c, where b is the provided point on the curve. diff --git a/website/other/bezier-rs-demos/src/components/BezierDemo.ts b/website/other/bezier-rs-demos/src/components/BezierDemo.ts index 4f3bc0bc..4ff6a0e7 100644 --- a/website/other/bezier-rs-demos/src/components/BezierDemo.ts +++ b/website/other/bezier-rs-demos/src/components/BezierDemo.ts @@ -1,7 +1,7 @@ import { WasmBezier } from "@/../wasm/pkg"; import bezierFeatures, { BezierFeatureKey } from "@/features/bezier-features"; import { renderDemo } from "@/utils/render"; -import { getConstructorKey, getCurveType, BezierCallback, BezierCurveType, SliderOption, WasmBezierManipulatorKey, ComputeType, Demo } from "@/utils/types"; +import { getConstructorKey, getCurveType, BezierCallback, BezierCurveType, SliderOption, WasmBezierManipulatorKey, TVariant, Demo } from "@/utils/types"; const SELECTABLE_RANGE = 10; @@ -24,7 +24,7 @@ class BezierDemo extends HTMLElement implements Demo { triggerOnMouseMove!: boolean; - computeType!: ComputeType; + tVariant!: TVariant; // Data bezier!: WasmBezier; @@ -40,12 +40,12 @@ class BezierDemo extends HTMLElement implements Demo { sliderUnits!: Record; static get observedAttributes(): string[] { - return ["computetype"]; + return ["tvariant"]; } attributeChangedCallback(name: string, oldValue: string, newValue: string): void { - if (name === "computetype" && oldValue) { - this.computeType = (newValue || "Parametric") as ComputeType; + if (name === "tvariant" && oldValue) { + this.tVariant = (newValue || "Parametric") as TVariant; const figure = this.querySelector("figure") as HTMLElement; this.drawDemo(figure); } @@ -57,7 +57,7 @@ class BezierDemo extends HTMLElement implements Demo { this.key = this.getAttribute("key") as BezierFeatureKey; this.sliderOptions = JSON.parse(this.getAttribute("sliderOptions") || "[]"); this.triggerOnMouseMove = this.getAttribute("triggerOnMouseMove") === "true"; - this.computeType = (this.getAttribute("computetype") || "Parametric") as ComputeType; + this.tVariant = (this.getAttribute("tvariant") || "Parametric") as TVariant; this.callback = bezierFeatures[this.key].callback as BezierCallback; const curveType = getCurveType(this.points.length); @@ -78,7 +78,7 @@ class BezierDemo extends HTMLElement implements Demo { } drawDemo(figure: HTMLElement, mouseLocation?: [number, number]): void { - figure.innerHTML = this.callback(this.bezier, this.sliderData, mouseLocation, this.computeType); + figure.innerHTML = this.callback(this.bezier, this.sliderData, mouseLocation, this.tVariant); } onMouseDown(event: MouseEvent): void { diff --git a/website/other/bezier-rs-demos/src/components/BezierDemoPane.ts b/website/other/bezier-rs-demos/src/components/BezierDemoPane.ts index d3da7334..42aad5fb 100644 --- a/website/other/bezier-rs-demos/src/components/BezierDemoPane.ts +++ b/website/other/bezier-rs-demos/src/components/BezierDemoPane.ts @@ -1,6 +1,6 @@ import bezierFeatures, { BezierFeatureKey } from "@/features/bezier-features"; import { renderDemoPane } from "@/utils/render"; -import { BezierCurveType, BEZIER_CURVE_TYPE, ComputeType, BezierDemoOptions, SliderOption, Demo, DemoPane, BezierDemoArgs } from "@/utils/types"; +import { BezierCurveType, BEZIER_CURVE_TYPE, TVariant, BezierDemoOptions, SliderOption, Demo, DemoPane, BezierDemoArgs } from "@/utils/types"; const demoDefaults = { Linear: { @@ -36,24 +36,23 @@ class BezierDemoPane extends HTMLElement implements DemoPane { triggerOnMouseMove!: boolean; - chooseComputeType!: boolean; + chooseTVariant!: boolean; // Data demos!: BezierDemoArgs[]; id!: string; - computeType!: ComputeType; + tVariant!: TVariant; connectedCallback(): void { - this.computeType = "Parametric"; - + this.tVariant = "Parametric"; this.key = (this.getAttribute("name") || "") as BezierFeatureKey; this.id = `bezier/${this.key}`; this.name = bezierFeatures[this.key].name; this.demoOptions = JSON.parse(this.getAttribute("demoOptions") || "[]"); this.triggerOnMouseMove = this.getAttribute("triggerOnMouseMove") === "true"; - this.chooseComputeType = this.getAttribute("chooseComputeType") === "true"; + this.chooseTVariant = this.getAttribute("chooseTVariant") === "true"; // Use quadratic slider options as a default if sliders are not provided for the other curve types. const defaultSliderOptions: SliderOption[] = this.demoOptions.Quadratic?.sliderOptions || []; this.demos = BEZIER_CURVE_TYPE.map((curveType: BezierCurveType) => { @@ -80,7 +79,7 @@ class BezierDemoPane extends HTMLElement implements DemoPane { bezierDemo.setAttribute("key", this.key); bezierDemo.setAttribute("sliderOptions", JSON.stringify(demo.sliderOptions)); bezierDemo.setAttribute("triggerOnMouseMove", String(this.triggerOnMouseMove)); - bezierDemo.setAttribute("computetype", this.computeType); + bezierDemo.setAttribute("tvariant", this.tVariant); return bezierDemo; } } diff --git a/website/other/bezier-rs-demos/src/components/SubpathDemo.ts b/website/other/bezier-rs-demos/src/components/SubpathDemo.ts index 01bdd1f1..3fce09c4 100644 --- a/website/other/bezier-rs-demos/src/components/SubpathDemo.ts +++ b/website/other/bezier-rs-demos/src/components/SubpathDemo.ts @@ -2,7 +2,7 @@ import { WasmSubpath } from "@/../wasm/pkg"; import subpathFeatures, { SubpathFeatureKey } from "@/features/subpath-features"; import { renderDemo } from "@/utils/render"; -import { SubpathCallback, WasmSubpathInstance, WasmSubpathManipulatorKey, SliderOption, ComputeType } from "@/utils/types"; +import { SubpathCallback, WasmSubpathInstance, WasmSubpathManipulatorKey, SliderOption, TVariant } from "@/utils/types"; const SELECTABLE_RANGE = 10; const POINT_INDEX_TO_MANIPULATOR: WasmSubpathManipulatorKey[] = ["set_anchor", "set_in_handle", "set_out_handle"]; @@ -21,7 +21,7 @@ class SubpathDemo extends HTMLElement { triggerOnMouseMove!: boolean; - computeType!: ComputeType; + tVariant!: TVariant; // Data subpath!: WasmSubpath; @@ -37,12 +37,12 @@ class SubpathDemo extends HTMLElement { sliderUnits!: Record; static get observedAttributes(): string[] { - return ["computetype"]; + return ["tvariant"]; } attributeChangedCallback(name: string, oldValue: string, newValue: string): void { - if (name === "computetype" && oldValue) { - this.computeType = (newValue || "Parametric") as ComputeType; + if (name === "tvariant" && oldValue) { + this.tVariant = (newValue || "Parametric") as TVariant; const figure = this.querySelector("figure") as HTMLElement; this.drawDemo(figure); } @@ -55,7 +55,7 @@ class SubpathDemo extends HTMLElement { this.sliderOptions = JSON.parse(this.getAttribute("sliderOptions") || "[]"); this.triggerOnMouseMove = this.getAttribute("triggerOnMouseMove") === "true"; this.closed = this.getAttribute("closed") === "true"; - this.computeType = (this.getAttribute("computetype") || "Parametric") as ComputeType; + this.tVariant = (this.getAttribute("tvariant") || "Parametric") as TVariant; this.callback = subpathFeatures[this.key].callback as SubpathCallback; this.subpath = WasmSubpath.from_triples(this.triples, this.closed) as WasmSubpathInstance; @@ -73,7 +73,7 @@ class SubpathDemo extends HTMLElement { } drawDemo(figure: HTMLElement, mouseLocation?: [number, number]): void { - figure.innerHTML = this.callback(this.subpath, this.sliderData, mouseLocation, this.computeType); + figure.innerHTML = this.callback(this.subpath, this.sliderData, mouseLocation, this.tVariant); } onMouseDown(event: MouseEvent): void { diff --git a/website/other/bezier-rs-demos/src/components/SubpathDemoPane.ts b/website/other/bezier-rs-demos/src/components/SubpathDemoPane.ts index 82ea8cf8..2cba77f2 100644 --- a/website/other/bezier-rs-demos/src/components/SubpathDemoPane.ts +++ b/website/other/bezier-rs-demos/src/components/SubpathDemoPane.ts @@ -1,6 +1,6 @@ import subpathFeatures, { SubpathFeatureKey } from "@/features/subpath-features"; import { renderDemoPane } from "@/utils/render"; -import { ComputeType, Demo, DemoPane, SliderOption, SubpathDemoArgs } from "@/utils/types"; +import { TVariant, Demo, DemoPane, SliderOption, SubpathDemoArgs } from "@/utils/types"; class SubpathDemoPane extends HTMLElement implements DemoPane { // Props @@ -12,14 +12,14 @@ class SubpathDemoPane extends HTMLElement implements DemoPane { triggerOnMouseMove!: boolean; - chooseComputeType!: boolean; + chooseTVariant!: boolean; // Data demos!: SubpathDemoArgs[]; id!: string; - computeType!: ComputeType; + tVariant!: TVariant; connectedCallback(): void { this.demos = [ @@ -47,14 +47,13 @@ class SubpathDemoPane extends HTMLElement implements DemoPane { closed: true, }, ]; - this.computeType = "Parametric"; - + this.tVariant = "Parametric"; this.key = (this.getAttribute("name") || "") as SubpathFeatureKey; this.id = `subpath/${this.key}`; this.name = subpathFeatures[this.key].name; this.sliderOptions = JSON.parse(this.getAttribute("sliderOptions") || "[]"); this.triggerOnMouseMove = this.getAttribute("triggerOnMouseMove") === "true"; - this.chooseComputeType = this.getAttribute("chooseComputeType") === "true"; + this.chooseTVariant = this.getAttribute("chooseTVariant") === "true"; this.render(); } @@ -71,7 +70,7 @@ class SubpathDemoPane extends HTMLElement implements DemoPane { subpathDemo.setAttribute("key", this.key); subpathDemo.setAttribute("sliderOptions", JSON.stringify(this.sliderOptions)); subpathDemo.setAttribute("triggerOnMouseMove", String(this.triggerOnMouseMove)); - subpathDemo.setAttribute("computetype", this.computeType); + subpathDemo.setAttribute("tvariant", this.tVariant); return subpathDemo; } } diff --git a/website/other/bezier-rs-demos/src/features/bezier-features.ts b/website/other/bezier-rs-demos/src/features/bezier-features.ts index 42f64ca7..d25d2ff0 100644 --- a/website/other/bezier-rs-demos/src/features/bezier-features.ts +++ b/website/other/bezier-rs-demos/src/features/bezier-features.ts @@ -1,6 +1,6 @@ import { WasmBezier } from "@/../wasm/pkg"; -import { tSliderOptions, tErrorOptions, tMinimumSeperationOptions } from "@/utils/options"; -import { ComputeType, BezierDemoOptions, WasmBezierInstance, BezierCallback } from "@/utils/types"; +import { tSliderOptions, errorOptions, minimumSeparationOptions } from "@/utils/options"; +import { TVariant, BezierDemoOptions, WasmBezierInstance, BezierCallback } from "@/utils/types"; const bezierFeatures = { constructor: { @@ -67,13 +67,13 @@ const bezierFeatures = { }, evaluate: { name: "Evaluate", - callback: (bezier: WasmBezierInstance, options: Record, _: undefined, computeType: ComputeType): string => bezier.evaluate(options.computeArgument, computeType), + callback: (bezier: WasmBezierInstance, options: Record, _: undefined, tVariant: TVariant): string => bezier.evaluate(options.t, tVariant), demoOptions: { Quadratic: { - sliderOptions: [{ ...tSliderOptions, variable: "computeArgument" }], + sliderOptions: [tSliderOptions], }, }, - chooseComputeType: true, + chooseTVariant: true, }, "lookup-table": { name: "Lookup Table", @@ -118,25 +118,27 @@ const bezierFeatures = { }, tangent: { name: "Tangent", - callback: (bezier: WasmBezierInstance, options: Record): string => bezier.tangent(options.t), + callback: (bezier: WasmBezierInstance, options: Record, _: undefined, tVariant: TVariant): string => bezier.tangent(options.t, tVariant), demoOptions: { Quadratic: { sliderOptions: [tSliderOptions], }, }, + chooseTVariant: true, }, normal: { name: "Normal", - callback: (bezier: WasmBezierInstance, options: Record): string => bezier.normal(options.t), + callback: (bezier: WasmBezierInstance, options: Record, _: undefined, tVariant: TVariant): string => bezier.normal(options.t, tVariant), demoOptions: { Quadratic: { sliderOptions: [tSliderOptions], }, }, + chooseTVariant: true, }, curvature: { name: "Curvature", - callback: (bezier: WasmBezierInstance, options: Record): string => bezier.curvature(options.t), + callback: (bezier: WasmBezierInstance, options: Record, _: undefined, tVariant: TVariant): string => bezier.curvature(options.t, tVariant), demoOptions: { Linear: { disabled: true, @@ -145,19 +147,21 @@ const bezierFeatures = { sliderOptions: [tSliderOptions], }, }, + chooseTVariant: true, }, split: { name: "Split", - callback: (bezier: WasmBezierInstance, options: Record): string => bezier.split(options.t), + callback: (bezier: WasmBezierInstance, options: Record, _: undefined, tVariant: TVariant): string => bezier.split(options.t, tVariant), demoOptions: { Quadratic: { sliderOptions: [tSliderOptions], }, }, + chooseTVariant: true, }, trim: { name: "Trim", - callback: (bezier: WasmBezierInstance, options: Record): string => bezier.trim(options.t1, options.t2), + callback: (bezier: WasmBezierInstance, options: Record, _: undefined, tVariant: TVariant): string => bezier.trim(options.t1, options.t2, tVariant), demoOptions: { Quadratic: { sliderOptions: [ @@ -178,6 +182,7 @@ const bezierFeatures = { ], }, }, + chooseTVariant: true, }, project: { name: "Project", @@ -400,11 +405,11 @@ const bezierFeatures = { [180, 10], [90, 120], ]; - return bezier.intersect_quadratic_segment(quadratic, options.error, options.minimum_seperation); + return bezier.intersect_quadratic_segment(quadratic, options.error, options.minimum_separation); }, demoOptions: { Quadratic: { - sliderOptions: [tErrorOptions, tMinimumSeperationOptions], + sliderOptions: [errorOptions, minimumSeparationOptions], }, }, }, @@ -417,11 +422,11 @@ const bezierFeatures = { [40, 120], [175, 140], ]; - return bezier.intersect_cubic_segment(cubic, options.error, options.minimum_seperation); + return bezier.intersect_cubic_segment(cubic, options.error, options.minimum_separation); }, demoOptions: { Quadratic: { - sliderOptions: [tErrorOptions, tMinimumSeperationOptions], + sliderOptions: [errorOptions, minimumSeparationOptions], }, }, }, @@ -430,7 +435,7 @@ const bezierFeatures = { callback: (bezier: WasmBezierInstance, options: Record): string => bezier.intersect_self(options.error), demoOptions: { Quadratic: { - sliderOptions: [tErrorOptions], + sliderOptions: [errorOptions], }, Cubic: { customPoints: [ @@ -470,12 +475,13 @@ const bezierFeatures = { }, "de-casteljau-points": { name: "De Casteljau Points", - callback: (bezier: WasmBezierInstance, options: Record): string => bezier.de_casteljau_points(options.t), + callback: (bezier: WasmBezierInstance, options: Record, _: undefined, tVariant: TVariant): string => bezier.de_casteljau_points(options.t, tVariant), demoOptions: { Quadratic: { sliderOptions: [tSliderOptions], }, }, + chooseTVariant: true, }, }; @@ -485,6 +491,6 @@ export type BezierFeatureOptions = { callback: BezierCallback; demoOptions?: Partial; triggerOnMouseMove?: boolean; - chooseComputeType?: boolean; + chooseTVariant?: boolean; }; export default bezierFeatures as Record; 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 2991e784..fedefa0a 100644 --- a/website/other/bezier-rs-demos/src/features/subpath-features.ts +++ b/website/other/bezier-rs-demos/src/features/subpath-features.ts @@ -1,5 +1,5 @@ import { tSliderOptions } from "@/utils/options"; -import { ComputeType, SliderOption, SubpathCallback, WasmSubpathInstance } from "@/utils/types"; +import { TVariant, SliderOption, SubpathCallback, WasmSubpathInstance } from "@/utils/types"; const subpathFeatures = { constructor: { @@ -8,10 +8,10 @@ const subpathFeatures = { }, insert: { name: "Insert", - callback: (subpath: WasmSubpathInstance, options: Record, _: undefined, computeType: ComputeType): string => subpath.insert(options.computeArgument, computeType), - sliderOptions: [{ ...tSliderOptions, variable: "computeArgument" }], + callback: (subpath: WasmSubpathInstance, options: Record, _: undefined, tVariant: TVariant): string => subpath.insert(options.t, tVariant), + sliderOptions: [tSliderOptions], // TODO: Uncomment this after implementing the Euclidean version - // chooseComputeType: true, + // chooseTVariant: true, }, length: { name: "Length", @@ -19,9 +19,9 @@ const subpathFeatures = { }, evaluate: { name: "Evaluate", - callback: (subpath: WasmSubpathInstance, options: Record, _: undefined, computeType: ComputeType): string => subpath.evaluate(options.computeArgument, computeType), - sliderOptions: [{ ...tSliderOptions, variable: "computeArgument" }], - chooseComputeType: true, + callback: (subpath: WasmSubpathInstance, options: Record, _: undefined, tVariant: TVariant): string => subpath.evaluate(options.t, tVariant), + sliderOptions: [tSliderOptions], + chooseTVariant: true, }, project: { name: "Project", @@ -68,10 +68,10 @@ const subpathFeatures = { }, split: { name: "Split", - callback: (subpath: WasmSubpathInstance, options: Record, _: undefined, computeType: ComputeType): string => subpath.split(options.computeArgument, computeType), - sliderOptions: [{ ...tSliderOptions, variable: "computeArgument" }], + callback: (subpath: WasmSubpathInstance, options: Record, _: undefined, tVariant: TVariant): string => subpath.split(options.t, tVariant), + sliderOptions: [tSliderOptions], // TODO: Uncomment this after implementing the Euclidean version - // chooseComputeType: true, + // chooseTVariant: true, }, }; @@ -81,6 +81,6 @@ export type SubpathFeatureOptions = { callback: SubpathCallback; sliderOptions?: SliderOption[]; triggerOnMouseMove?: boolean; - chooseComputeType?: boolean; + chooseTVariant?: boolean; }; export default subpathFeatures as Record; diff --git a/website/other/bezier-rs-demos/src/main.ts b/website/other/bezier-rs-demos/src/main.ts index da97e0d3..7bd0589a 100644 --- a/website/other/bezier-rs-demos/src/main.ts +++ b/website/other/bezier-rs-demos/src/main.ts @@ -31,7 +31,7 @@ function renderBezierPane(featureName: BezierFeatureKey, container: HTMLElement demo.setAttribute("name", featureName); demo.setAttribute("demoOptions", JSON.stringify(feature.demoOptions || {})); demo.setAttribute("triggerOnMouseMove", String(feature.triggerOnMouseMove)); - demo.setAttribute("chooseComputeType", String(feature.chooseComputeType)); + demo.setAttribute("chooseTVariant", String(feature.chooseTVariant)); container?.append(demo); } @@ -42,7 +42,7 @@ function renderSubpathPane(featureName: SubpathFeatureKey, container: HTMLElemen demo.setAttribute("name", featureName); demo.setAttribute("sliderOptions", JSON.stringify(feature.sliderOptions || [])); demo.setAttribute("triggerOnMouseMove", String(feature.triggerOnMouseMove)); - demo.setAttribute("chooseComputeType", String(feature.chooseComputeType)); + demo.setAttribute("chooseTVariant", String(feature.chooseTVariant)); container?.append(demo); } diff --git a/website/other/bezier-rs-demos/src/style.css b/website/other/bezier-rs-demos/src/style.css index f7345553..ad208ea3 100644 --- a/website/other/bezier-rs-demos/src/style.css +++ b/website/other/bezier-rs-demos/src/style.css @@ -30,7 +30,7 @@ body > h2 { justify-content: center; } -.compute-type-choice { +.t-variant-choice { margin-top: 20px; } diff --git a/website/other/bezier-rs-demos/src/utils/options.ts b/website/other/bezier-rs-demos/src/utils/options.ts index 11ef20a6..14de87c3 100644 --- a/website/other/bezier-rs-demos/src/utils/options.ts +++ b/website/other/bezier-rs-demos/src/utils/options.ts @@ -6,7 +6,7 @@ export const tSliderOptions = { variable: "t", }; -export const tErrorOptions = { +export const errorOptions = { variable: "error", min: 0.1, max: 2, @@ -14,7 +14,7 @@ export const tErrorOptions = { default: 0.5, }; -export const tMinimumSeperationOptions = { +export const minimumSeparationOptions = { variable: "minimum_seperation", min: 0.001, max: 0.25, diff --git a/website/other/bezier-rs-demos/src/utils/render.ts b/website/other/bezier-rs-demos/src/utils/render.ts index 2f37fe2d..a88c1cf7 100644 --- a/website/other/bezier-rs-demos/src/utils/render.ts +++ b/website/other/bezier-rs-demos/src/utils/render.ts @@ -1,4 +1,4 @@ -import { ComputeType, Demo, DemoPane, SliderOption } from "@/utils/types"; +import { TVariant, Demo, DemoPane, SliderOption } from "@/utils/types"; export function renderDemo(demo: Demo): void { const header = document.createElement("h4"); @@ -53,27 +53,27 @@ export function renderDemoPane(demoPane: DemoPane): void { header.className = "demo-pane-header"; header.append(headerAnchorLink); - const computeTypeContainer = document.createElement("div"); - computeTypeContainer.className = "compute-type-choice"; + const tVariantContainer = document.createElement("div"); + tVariantContainer.className = "t-variant-choice"; - const computeTypeLabel = document.createElement("strong"); - computeTypeLabel.innerText = "ComputeType:"; - computeTypeContainer.append(computeTypeLabel); + const tVariantLabel = document.createElement("strong"); + tVariantLabel.innerText = "TValue Variant:"; + tVariantContainer.append(tVariantLabel); - const radioInputs = ["Parametric", "Euclidean"].map((computeType) => { - const id = `${demoPane.id}-${computeType}`; + const radioInputs = ["Parametric", "Euclidean"].map((tVariant) => { + const id = `${demoPane.id}-${tVariant}`; const radioInput = document.createElement("input"); radioInput.type = "radio"; radioInput.id = id; - radioInput.value = computeType; - radioInput.name = "ComputeType"; - radioInput.checked = computeType === "Parametric"; - computeTypeContainer.append(radioInput); + radioInput.value = tVariant; + radioInput.name = `TVariant - ${demoPane.id}`; + radioInput.checked = tVariant === "Parametric"; + tVariantContainer.append(radioInput); const label = document.createElement("label"); label.htmlFor = id; - label.innerText = computeType; - computeTypeContainer.append(label); + label.innerText = tVariant; + tVariantContainer.append(label); return radioInput; }); @@ -88,16 +88,16 @@ export function renderDemoPane(demoPane: DemoPane): void { radioInputs.forEach((radioInput: HTMLElement) => { radioInput.addEventListener("input", (event: Event): void => { - demoPane.computeType = (event.target as HTMLInputElement).value as ComputeType; - demoComponent.setAttribute("computetype", demoPane.computeType); + demoPane.tVariant = (event.target as HTMLInputElement).value as TVariant; + demoComponent.setAttribute("tvariant", demoPane.tVariant); }); }); demoRow.append(demoComponent); }); container.append(header); - if (demoPane.chooseComputeType) { - container.append(computeTypeContainer); + if (demoPane.chooseTVariant) { + container.append(tVariantContainer); } container.append(demoRow); diff --git a/website/other/bezier-rs-demos/src/utils/types.ts b/website/other/bezier-rs-demos/src/utils/types.ts index 30ce0370..76d61064 100644 --- a/website/other/bezier-rs-demos/src/utils/types.ts +++ b/website/other/bezier-rs-demos/src/utils/types.ts @@ -11,10 +11,10 @@ export type WasmSubpathManipulatorKey = "set_anchor" | "set_in_handle" | "set_ou export const BEZIER_CURVE_TYPE = ["Linear", "Quadratic", "Cubic"] as const; export type BezierCurveType = typeof BEZIER_CURVE_TYPE[number]; -export type ComputeType = "Euclidean" | "Parametric"; +export type TVariant = "Euclidean" | "Parametric"; -export type BezierCallback = (bezier: WasmBezierInstance, options: Record, mouseLocation?: [number, number], computeType?: ComputeType) => string; -export type SubpathCallback = (subpath: WasmSubpathInstance, options: Record, mouseLocation?: [number, number], computeType?: ComputeType) => string; +export type BezierCallback = (bezier: WasmBezierInstance, options: Record, mouseLocation?: [number, number], tVariant?: TVariant) => string; +export type SubpathCallback = (subpath: WasmSubpathInstance, options: Record, mouseLocation?: [number, number], tVariant?: TVariant) => string; export type BezierDemoOptions = { [key in BezierCurveType]: { @@ -85,7 +85,7 @@ export interface DemoPane extends HTMLElement { name: string; demos: DemoArgs[]; id: string; - chooseComputeType: boolean; - computeType: ComputeType; + chooseTVariant: boolean; + tVariant: TVariant; buildDemo(demo: DemoArgs): Demo; } diff --git a/website/other/bezier-rs-demos/wasm/src/bezier.rs b/website/other/bezier-rs-demos/wasm/src/bezier.rs index aaef9b63..6f63ca1c 100644 --- a/website/other/bezier-rs-demos/wasm/src/bezier.rs +++ b/website/other/bezier-rs-demos/wasm/src/bezier.rs @@ -1,5 +1,5 @@ use crate::svg_drawing::*; -use bezier_rs::{ArcStrategy, ArcsOptions, Bezier, ComputeType, ProjectionOptions}; +use bezier_rs::{ArcStrategy, ArcsOptions, Bezier, ProjectionOptions, TValue}; use glam::DVec2; use serde::{Deserialize, Serialize}; use wasm_bindgen::prelude::*; @@ -41,6 +41,14 @@ fn convert_wasm_maximize_arcs(wasm_enum_value: WasmMaximizeArcs) -> ArcStrategy } } +fn parse_t_variant(t_variant: &String, t: f64) -> TValue { + match t_variant.as_str() { + "Parametric" => TValue::Parametric(t), + "Euclidean" => TValue::Euclidean(t), + _ => panic!("Unexpected TValue string: '{}'", t_variant), + } +} + #[wasm_bindgen] impl WasmBezier { /// Expect js_points to be a list of 2 pairs. @@ -128,13 +136,10 @@ impl WasmBezier { wrap_svg_tag(format!("{bezier}{}", draw_text(format!("Length: {:.2}", self.0.length(None)), TEXT_OFFSET_X, TEXT_OFFSET_Y, BLACK))) } - pub fn evaluate(&self, t: f64, compute_type: String) -> String { + pub fn evaluate(&self, raw_t: f64, t_variant: String) -> String { let bezier = self.get_bezier_path(); - let point = match compute_type.as_str() { - "Euclidean" => self.0.evaluate(ComputeType::Euclidean(t)), - "Parametric" => self.0.evaluate(ComputeType::Parametric(t)), - _ => panic!("Unexpected ComputeType string: '{}'", compute_type), - }; + let t = parse_t_variant(&t_variant, raw_t); + let point = self.0.evaluate(t); let content = format!("{bezier}{}", draw_circle(point, 4., RED, 1.5, WHITE)); wrap_svg_tag(content) } @@ -169,11 +174,12 @@ impl WasmBezier { wrap_svg_tag(content) } - pub fn tangent(&self, t: f64) -> String { + pub fn tangent(&self, raw_t: f64, t_variant: String) -> String { let bezier = self.get_bezier_path(); + let t = parse_t_variant(&t_variant, raw_t); let tangent_point = self.0.tangent(t); - let intersection_point = self.0.evaluate(ComputeType::Parametric(t)); + let intersection_point = self.0.evaluate(t); let tangent_end = intersection_point + tangent_point * SCALE_UNIT_VECTOR_FACTOR; let content = format!( @@ -185,11 +191,12 @@ impl WasmBezier { wrap_svg_tag(content) } - pub fn normal(&self, t: f64) -> String { + pub fn normal(&self, raw_t: f64, t_variant: String) -> String { let bezier = self.get_bezier_path(); + let t = parse_t_variant(&t_variant, raw_t); let normal_point = self.0.normal(t); - let intersection_point = self.0.evaluate(ComputeType::Parametric(t)); + let intersection_point = self.0.evaluate(t); let normal_end = intersection_point + normal_point * SCALE_UNIT_VECTOR_FACTOR; let content = format!( @@ -201,11 +208,13 @@ impl WasmBezier { wrap_svg_tag(content) } - pub fn curvature(&self, t: f64) -> String { + pub fn curvature(&self, raw_t: f64, t_variant: String) -> String { let bezier = self.get_bezier_path(); + let t = parse_t_variant(&t_variant, raw_t); + let radius = 1. / self.0.curvature(t); let normal_point = self.0.normal(t); - let intersection_point = self.0.evaluate(ComputeType::Parametric(t)); + let intersection_point = self.0.evaluate(t); let curvature_center = intersection_point + normal_point * radius; @@ -219,7 +228,8 @@ impl WasmBezier { wrap_svg_tag(content) } - pub fn split(&self, t: f64) -> String { + pub fn split(&self, raw_t: f64, t_variant: String) -> String { + let t = parse_t_variant(&t_variant, raw_t); let beziers: [Bezier; 2] = self.0.split(t); let mut original_bezier_svg = String::new(); @@ -252,7 +262,8 @@ impl WasmBezier { wrap_svg_tag(format!("{original_bezier_svg}{bezier_svg_1}{bezier_svg_2}")) } - pub fn trim(&self, t1: f64, t2: f64) -> String { + pub fn trim(&self, raw_t1: f64, raw_t2: f64, t_variant: String) -> String { + let (t1, t2) = (parse_t_variant(&t_variant, raw_t1), parse_t_variant(&t_variant, raw_t2)); let trimmed_bezier = self.0.trim(t1, t2); let mut trimmed_bezier_svg = String::new(); @@ -269,7 +280,7 @@ impl WasmBezier { pub fn project(&self, x: f64, y: f64) -> String { let projected_t_value = self.0.project(DVec2::new(x, y), ProjectionOptions::default()); - let projected_point = self.0.evaluate(ComputeType::Parametric(projected_t_value)); + let projected_point = self.0.evaluate(TValue::Parametric(projected_t_value)); let bezier = self.get_bezier_path(); let content = format!("{bezier}{}", draw_line(projected_point.x, projected_point.y, x, y, RED, 1.),); @@ -285,7 +296,7 @@ impl WasmBezier { .zip([RED, GREEN]) .flat_map(|(t_value_list, color)| { t_value_list.iter().map(|&t_value| { - let point = self.0.evaluate(ComputeType::Parametric(t_value)); + let point = self.0.evaluate(TValue::Parametric(t_value)); draw_circle(point, 3., color, 1.5, WHITE) }) }) @@ -320,7 +331,7 @@ impl WasmBezier { let circles: String = inflections .iter() .map(|&t_value| { - let point = self.0.evaluate(ComputeType::Parametric(t_value)); + let point = self.0.evaluate(TValue::Parametric(t_value)); draw_circle(point, 3., RED, 1.5, WHITE) }) .fold("".to_string(), |acc, circle| acc + &circle); @@ -328,7 +339,8 @@ impl WasmBezier { wrap_svg_tag(content) } - pub fn de_casteljau_points(&self, t: f64) -> String { + pub fn de_casteljau_points(&self, raw_t: f64, t_variant: String) -> String { + let t = parse_t_variant(&t_variant, raw_t); let points: Vec> = self.0.de_casteljau_points(t); let bezier_svg = self.get_bezier_path(); @@ -413,7 +425,7 @@ impl WasmBezier { .intersect(&line, None, None) .iter() .map(|intersection_t| { - let point = &self.0.evaluate(ComputeType::Parametric(*intersection_t)); + let point = &self.0.evaluate(TValue::Parametric(*intersection_t)); draw_circle(*point, 4., RED, 1.5, WHITE) }) .fold(String::new(), |acc, item| format!("{acc}{item}")); @@ -433,7 +445,7 @@ impl WasmBezier { .intersect(&quadratic, Some(error), Some(minimum_separation)) .iter() .map(|intersection_t| { - let point = &self.0.evaluate(ComputeType::Parametric(*intersection_t)); + let point = &self.0.evaluate(TValue::Parametric(*intersection_t)); draw_circle(*point, 4., RED, 1.5, WHITE) }) .fold(String::new(), |acc, item| format!("{acc}{item}")); @@ -453,7 +465,7 @@ impl WasmBezier { .intersect(&cubic, Some(error), Some(minimum_separation)) .iter() .map(|intersection_t| { - let point = &self.0.evaluate(ComputeType::Parametric(*intersection_t)); + let point = &self.0.evaluate(TValue::Parametric(*intersection_t)); draw_circle(*point, 4., RED, 1.5, WHITE) }) .fold(String::new(), |acc, item| format!("{acc}{item}")); @@ -469,7 +481,7 @@ impl WasmBezier { .self_intersections(Some(error)) .iter() .map(|intersection_t| { - let point = &self.0.evaluate(ComputeType::Parametric(intersection_t[0])); + let point = &self.0.evaluate(TValue::Parametric(intersection_t[0])); draw_circle(*point, 4., RED, 1.5, WHITE) }) .fold(bezier_curve_svg, |acc, item| format!("{acc}{item}")); @@ -497,7 +509,7 @@ impl WasmBezier { .rectangle_intersections(points[0], points[1]) .iter() .map(|intersection_t| { - let point = &self.0.evaluate(ComputeType::Parametric(*intersection_t)); + let point = &self.0.evaluate(TValue::Parametric(*intersection_t)); draw_circle(*point, 4., RED, 1.5, WHITE) }) .fold(String::new(), |acc, item| format!("{acc}{item}")); diff --git a/website/other/bezier-rs-demos/wasm/src/subpath.rs b/website/other/bezier-rs-demos/wasm/src/subpath.rs index 645196a9..48add023 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, ComputeType, ManipulatorGroup, ProjectionOptions, Subpath}; +use bezier_rs::{Bezier, ManipulatorGroup, ProjectionOptions, Subpath, TValue}; use glam::DVec2; use std::fmt::Write; @@ -56,20 +56,20 @@ impl WasmSubpath { subpath_svg } - pub fn insert(&self, t: f64, compute_type: String) -> String { + pub fn insert(&self, t: f64, t_variant: String) -> String { let mut subpath = self.0.clone(); - let point = match compute_type.as_str() { + let point = match t_variant.as_str() { "Euclidean" => { - let parameter = ComputeType::Euclidean(t); + let parameter = TValue::Euclidean(t); subpath.insert(parameter); self.0.evaluate(parameter) } "Parametric" => { - let parameter = ComputeType::Parametric(t); + let parameter = TValue::Parametric(t); subpath.insert(parameter); self.0.evaluate(parameter) } - _ => panic!("Unexpected ComputeType string: '{}'", compute_type), + _ => panic!("Unexpected TValue string: '{}'", t_variant), }; let point_text = draw_circle(point, 4., RED, 1.5, WHITE); @@ -81,19 +81,19 @@ impl WasmSubpath { wrap_svg_tag(format!("{}{}", self.to_default_svg(), length_text)) } - pub fn evaluate(&self, t: f64, compute_type: String) -> String { - let point = match compute_type.as_str() { - "Euclidean" => self.0.evaluate(ComputeType::Euclidean(t)), - "Parametric" => self.0.evaluate(ComputeType::Parametric(t)), - _ => panic!("Unexpected ComputeType string: '{}'", compute_type), + 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 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(ComputeType::Parametric(t)); - let tangent_point = self.0.tangent(ComputeType::Parametric(t)); + let intersection_point = self.0.evaluate(TValue::Parametric(t)); + let tangent_point = self.0.tangent(TValue::Parametric(t)); let tangent_end = intersection_point + tangent_point * SCALE_UNIT_VECTOR_FACTOR; let point_text = draw_circle(intersection_point, 4., RED, 1.5, WHITE); @@ -103,8 +103,8 @@ impl WasmSubpath { } pub fn normal(&self, t: f64) -> String { - let intersection_point = self.0.evaluate(ComputeType::Parametric(t)); - let normal_point = self.0.normal(ComputeType::Parametric(t)); + let intersection_point = self.0.evaluate(TValue::Parametric(t)); + let normal_point = self.0.normal(TValue::Parametric(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 +115,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(ComputeType::Parametric((segment_index as f64 + projected_t) / (self.0.len_segments() as f64))); + let projected_point = self.0.evaluate(TValue::Parametric((segment_index as f64 + projected_t) / (self.0.len_segments() as f64))); 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 +143,7 @@ impl WasmSubpath { .intersections(&line, None, None) .iter() .map(|intersection_t| { - let point = self.0.evaluate(ComputeType::Parametric(*intersection_t)); + let point = self.0.evaluate(TValue::Parametric(*intersection_t)); draw_circle(point, 4., RED, 1.5, WHITE) }) .fold(String::new(), |acc, item| format!("{acc}{item}")); @@ -172,7 +172,7 @@ impl WasmSubpath { .intersections(&line, None, None) .iter() .map(|intersection_t| { - let point = self.0.evaluate(ComputeType::Parametric(*intersection_t)); + let point = self.0.evaluate(TValue::Parametric(*intersection_t)); draw_circle(point, 4., RED, 1.5, WHITE) }) .fold(String::new(), |acc, item| format!("{acc}{item}")); @@ -201,7 +201,7 @@ impl WasmSubpath { .intersections(&line, None, None) .iter() .map(|intersection_t| { - let point = self.0.evaluate(ComputeType::Parametric(*intersection_t)); + let point = self.0.evaluate(TValue::Parametric(*intersection_t)); draw_circle(point, 4., RED, 1.5, WHITE) }) .fold(String::new(), |acc, item| format!("{acc}{item}")); @@ -209,11 +209,11 @@ impl WasmSubpath { wrap_svg_tag(format!("{subpath_svg}{line_svg}{intersections_svg}")) } - pub fn split(&self, t: f64, compute_type: String) -> String { - let (main_subpath, optional_subpath) = match compute_type.as_str() { - "Euclidean" => self.0.split(ComputeType::Euclidean(t)), - "Parametric" => self.0.split(ComputeType::Parametric(t)), - _ => panic!("Unexpected ComputeType string: '{}'", compute_type), + 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 mut main_subpath_svg = String::new();