use super::*; use crate::utils::{TValue, TValueType}; /// 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 total_length = self.length(None); self.euclidean_to_parametric_with_total_length(ratio, error, total_length) } /// Convert a euclidean distance ratio along the `Bezier` curve to a parametric `t`-value. /// For performance reasons, this version of the [`euclidean_to_parametric`] function allows the caller to /// provide the total length of the curve so it doesn't have to be calculated every time the function is called. pub fn euclidean_to_parametric_with_total_length(&self, euclidean_t: f64, error: f64, total_length: f64) -> f64 { if euclidean_t < error { return 0.; } if 1. - euclidean_t < error { return 1.; } match self.handles { BezierHandles::Linear => euclidean_t, BezierHandles::Quadratic { handle } => { // Use Casteljau subdivision, noting that the length is more than the straight line distance from start to end but less than the straight line distance through the handles fn recurse(a0: DVec2, a1: DVec2, a2: DVec2, level: u8, desired_len: f64) -> (f64, f64) { let lower = a0.distance(a2); let upper = a0.distance(a1) + a1.distance(a2); if level >= 8 { let approx_len = (lower + upper) / 2.; return (approx_len, desired_len / approx_len); } let b1 = 0.5 * (a0 + a1); let c1 = 0.5 * (a1 + a2); let b2 = 0.5 * (b1 + c1); let (first_len, t) = recurse(a0, b1, b2, level + 1, desired_len); if first_len > desired_len { return (first_len, t * 0.5); } let (second_len, t) = recurse(b2, c1, a2, level + 1, desired_len - first_len); (first_len + second_len, t * 0.5 + 0.5) } recurse(self.start, handle, self.end, 0, total_length * euclidean_t).1 } BezierHandles::Cubic { handle_start, handle_end } => { // Use Casteljau subdivision, noting that the length is more than the straight line distance from start to end but less than the straight line distance through the handles fn recurse(a0: DVec2, a1: DVec2, a2: DVec2, a3: DVec2, level: u8, desired_len: f64) -> (f64, f64) { let lower = a0.distance(a3); let upper = a0.distance(a1) + a1.distance(a2) + a2.distance(a3); if level >= 8 { let approx_len = (lower + upper) / 2.; return (approx_len, desired_len / approx_len); } let b1 = 0.5 * (a0 + a1); let t0 = 0.5 * (a1 + a2); let c1 = 0.5 * (a2 + a3); let b2 = 0.5 * (b1 + t0); let c2 = 0.5 * (t0 + c1); let b3 = 0.5 * (b2 + c2); let (first_len, t) = recurse(a0, b1, b2, b3, level + 1, desired_len); if first_len > desired_len { return (first_len, t * 0.5); } let (second_len, t) = recurse(b3, c2, c1, a3, level + 1, desired_len - first_len); (first_len + second_len, t * 0.5 + 0.5) } recurse(self.start, handle_start, handle_end, self.end, 0, total_length * euclidean_t).1 } } .clamp(0., 1.) } /// 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: . let t_squared = t * t; let one_minus_t = 1. - t; let squared_one_minus_t = one_minus_t * one_minus_t; match self.handles { BezierHandles::Linear => self.start.lerp(self.end, t), BezierHandles::Quadratic { handle } => squared_one_minus_t * self.start + 2. * one_minus_t * t * handle + t_squared * self.end, BezierHandles::Cubic { handle_start, handle_end } => { let t_cubed = t_squared * t; let cubed_one_minus_t = squared_one_minus_t * one_minus_t; cubed_one_minus_t * self.start + 3. * squared_one_minus_t * t * handle_start + 3. * one_minus_t * t_squared * handle_end + t_cubed * self.end } } } /// 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: 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. /// If no value is provided for `steps`, then the function will default `steps` to be 10. /// pub fn compute_lookup_table(&self, steps: Option, tvalue_type: Option) -> impl Iterator + '_ { let steps = steps.unwrap_or(DEFAULT_LUT_STEP_SIZE); let tvalue_type = tvalue_type.unwrap_or(TValueType::Parametric); (0..=steps).map(move |t| { let tvalue = match tvalue_type { TValueType::Parametric => TValue::Parametric(t as f64 / steps as f64), TValueType::Euclidean => TValue::Euclidean(t as f64 / steps as f64), }; self.evaluate(tvalue) }) } /// Return an approximation of the length of the bezier curve. /// - `tolerance` - Tolerance used to approximate the curve. /// pub fn length(&self, tolerance: Option) -> f64 { match self.handles { BezierHandles::Linear => (self.start - self.end).length(), BezierHandles::Quadratic { handle } => { // Use Casteljau subdivision, noting that the length is more than the straight line distance from start to end but less than the straight line distance through the handles fn recurse(a0: DVec2, a1: DVec2, a2: DVec2, tolerance: f64, level: u8) -> f64 { let lower = a0.distance(a2); let upper = a0.distance(a1) + a1.distance(a2); if upper - lower <= 2. * tolerance || level >= 8 { return (lower + upper) / 2.; } let b1 = 0.5 * (a0 + a1); let c1 = 0.5 * (a1 + a2); let b2 = 0.5 * (b1 + c1); recurse(a0, b1, b2, 0.5 * tolerance, level + 1) + recurse(b2, c1, a2, 0.5 * tolerance, level + 1) } recurse(self.start, handle, self.end, tolerance.unwrap_or_default(), 0) } BezierHandles::Cubic { handle_start, handle_end } => { // Use Casteljau subdivision, noting that the length is more than the straight line distance from start to end but less than the straight line distance through the handles fn recurse(a0: DVec2, a1: DVec2, a2: DVec2, a3: DVec2, tolerance: f64, level: u8) -> f64 { let lower = a0.distance(a3); let upper = a0.distance(a1) + a1.distance(a2) + a2.distance(a3); if upper - lower <= 2. * tolerance || level >= 8 { return (lower + upper) / 2.; } let b1 = 0.5 * (a0 + a1); let t0 = 0.5 * (a1 + a2); let c1 = 0.5 * (a2 + a3); let b2 = 0.5 * (b1 + t0); let c2 = 0.5 * (t0 + c1); let b3 = 0.5 * (b2 + c2); recurse(a0, b1, b2, b3, 0.5 * tolerance, level + 1) + recurse(b3, c2, c1, a3, 0.5 * tolerance, level + 1) } recurse(self.start, handle_start, handle_end, self.end, tolerance.unwrap_or_default(), 0) } } } /// Return an approximation of the length centroid, together with the length, of the bezier curve. /// /// The length centroid is the center of mass for the arc length of the Bezier segment. /// An infinitely thin wire forming the Bezier segment's shape would balance at this point. /// /// - `tolerance` - Tolerance used to approximate the curve. pub fn length_centroid_and_length(&self, tolerance: Option) -> (DVec2, f64) { match self.handles { BezierHandles::Linear => ((self.start + self.end()) / 2., (self.start - self.end).length()), BezierHandles::Quadratic { handle } => { // Use Casteljau subdivision, noting that the length is more than the straight line distance from start to end but less than the straight line distance through the handles fn recurse(a0: DVec2, a1: DVec2, a2: DVec2, tolerance: f64, level: u8) -> (f64, DVec2) { let lower = a0.distance(a2); let upper = a0.distance(a1) + a1.distance(a2); if upper - lower <= 2. * tolerance || level >= 8 { let length = (lower + upper) / 2.; return (length, length * (a0 + a1 + a2) / 3.); } let b1 = 0.5 * (a0 + a1); let c1 = 0.5 * (a1 + a2); let b2 = 0.5 * (b1 + c1); let (length1, centroid_part1) = recurse(a0, b1, b2, 0.5 * tolerance, level + 1); let (length2, centroid_part2) = recurse(b2, c1, a2, 0.5 * tolerance, level + 1); (length1 + length2, centroid_part1 + centroid_part2) } let (length, centroid_parts) = recurse(self.start, handle, self.end, tolerance.unwrap_or_default(), 0); (centroid_parts / length, length) } BezierHandles::Cubic { handle_start, handle_end } => { // Use Casteljau subdivision, noting that the length is more than the straight line distance from start to end but less than the straight line distance through the handles fn recurse(a0: DVec2, a1: DVec2, a2: DVec2, a3: DVec2, tolerance: f64, level: u8) -> (f64, DVec2) { let lower = a0.distance(a3); let upper = a0.distance(a1) + a1.distance(a2) + a2.distance(a3); if upper - lower <= 2. * tolerance || level >= 8 { let length = (lower + upper) / 2.; return (length, length * (a0 + a1 + a2 + a3) / 4.); } let b1 = 0.5 * (a0 + a1); let t0 = 0.5 * (a1 + a2); let c1 = 0.5 * (a2 + a3); let b2 = 0.5 * (b1 + t0); let c2 = 0.5 * (t0 + c1); let b3 = 0.5 * (b2 + c2); let (length1, centroid_part1) = recurse(a0, b1, b2, b3, 0.5 * tolerance, level + 1); let (length2, centroid_part2) = recurse(b3, c2, c1, a3, 0.5 * tolerance, level + 1); (length1 + length2, centroid_part1 + centroid_part2) } let (length, centroid_parts) = recurse(self.start, handle_start, handle_end, self.end, tolerance.unwrap_or_default(), 0); (centroid_parts / length, length) } } } /// Return an approximation of the length centroid of the Bezier curve. /// /// The length centroid is the center of mass for the arc length of the Bezier segment. /// An infinitely thin wire with the Bezier segment's shape would balance at this point. /// /// - `tolerance` - Tolerance used to approximate the curve. /// pub fn length_centroid(&self, tolerance: Option) -> DVec2 { self.length_centroid_and_length(tolerance).0 } /// Returns the parametric `t`-value that corresponds to the closest point on the curve to the provided point. /// pub fn project(&self, point: DVec2) -> f64 { let sbasis = crate::symmetrical_basis::to_symmetrical_basis_pair(*self); let derivative = sbasis.derivative(); let dd = (sbasis - point).dot(&derivative); let roots = dd.roots(); let mut closest = 0.; let mut min_dist_squared = self.evaluate(TValue::Parametric(0.)).distance_squared(point); for time in roots { let distance = self.evaluate(TValue::Parametric(time)).distance_squared(point); if distance < min_dist_squared { closest = time; min_dist_squared = distance; } } if self.evaluate(TValue::Parametric(1.)).distance_squared(point) < min_dist_squared { closest = 1.; } closest } } #[cfg(test)] mod tests { use super::*; #[test] fn test_evaluate() { let p1 = DVec2::new(3., 5.); let p2 = DVec2::new(14., 3.); let p3 = DVec2::new(19., 14.); let p4 = DVec2::new(30., 21.); let bezier1 = Bezier::from_quadratic_dvec2(p1, p2, p3); 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(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), Some(TValueType::Parametric)).collect::>(); 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), Some(TValueType::Parametric)).collect::>(); assert_eq!( lookup_table2, vec![ bezier2.start(), bezier2.evaluate(TValue::Parametric(0.25)), bezier2.evaluate(TValue::Parametric(0.50)), bezier2.evaluate(TValue::Parametric(0.75)), bezier2.end() ] ); } #[test] fn test_length() { let p1 = DVec2::new(30., 50.); let p2 = DVec2::new(140., 30.); let p3 = DVec2::new(160., 170.); let p4 = DVec2::new(77., 129.); let bezier_linear = Bezier::from_linear_dvec2(p1, p2); assert!(utils::f64_compare(bezier_linear.length(None), p1.distance(p2), MAX_ABSOLUTE_DIFFERENCE)); let bezier_quadratic = Bezier::from_quadratic_dvec2(p1, p2, p3); assert!(utils::f64_compare(bezier_quadratic.length(None), 204., 1e-2)); let bezier_cubic = Bezier::from_cubic_dvec2(p1, p2, p3, p4); assert!(utils::f64_compare(bezier_cubic.length(None), 199., 1e-2)); } #[test] fn test_length_centroid() { let p1 = DVec2::new(30., 50.); let p2 = DVec2::new(140., 30.); let p3 = DVec2::new(160., 170.); let p4 = DVec2::new(77., 129.); let bezier_linear = Bezier::from_linear_dvec2(p1, p2); assert!(bezier_linear.length_centroid_and_length(None).0.abs_diff_eq((p1 + p2) / 2., MAX_ABSOLUTE_DIFFERENCE)); let bezier_quadratic = Bezier::from_quadratic_dvec2(p1, p2, p3); let expected = DVec2::new(112.81017736920136, 87.98713052477228); assert!(bezier_quadratic.length_centroid_and_length(None).0.abs_diff_eq(expected, MAX_ABSOLUTE_DIFFERENCE)); let bezier_cubic = Bezier::from_cubic_dvec2(p1, p2, p3, p4); let expected = DVec2::new(95.23597072432115, 88.0645175770206); assert!(bezier_cubic.length_centroid_and_length(None).0.abs_diff_eq(expected, MAX_ABSOLUTE_DIFFERENCE)); } #[test] fn test_project() { let bezier1 = Bezier::from_cubic_coordinates(4., 4., 23., 45., 10., 30., 56., 90.); assert_eq!(bezier1.project(DVec2::ZERO), 0.); assert_eq!(bezier1.project(DVec2::new(100., 100.)), 1.); let bezier2 = Bezier::from_quadratic_coordinates(0., 0., 0., 100., 100., 100.); assert_eq!(bezier2.project(DVec2::new(100., 0.)), 0.); let bezier3 = Bezier::from_cubic_coordinates(-50., -50., -50., -50., 50., -50., 50., -50.); assert_eq!(DVec2::new(0., -50.), bezier3.evaluate(TValue::Parametric(bezier3.project(DVec2::new(0., -50.))))); } }