From 01a97243892648e5d2b223a2279b5e107b89a80a Mon Sep 17 00:00:00 2001 From: Hannah Li Date: Fri, 18 Nov 2022 20:37:52 -0800 Subject: [PATCH] Bezier-rs: Add Euclidean parameterization to Bezier::evaluate (#828) * Add enum to evaluate for differenciating compute type * Add euclidean parameterization and update styling in the UI Co-authored-by: Rob Nadal * Update usage of evaluate in graphite * Add description * Code review changes * Update tests * Improve ComputeType ergonomics * Large code review/cleanup pass Co-authored-by: Rob Nadal Co-authored-by: Keavon Chambers --- .../tool/common_functionality/shape_editor.rs | 3 +- libraries/bezier-rs/src/bezier/core.rs | 14 +- libraries/bezier-rs/src/bezier/lookup.rs | 65 ++++++-- libraries/bezier-rs/src/bezier/solvers.rs | 35 ++--- libraries/bezier-rs/src/bezier/transform.rs | 94 +++++++----- libraries/bezier-rs/src/lib.rs | 1 + libraries/bezier-rs/src/utils.rs | 7 + website/other/bezier-rs-demos/src/App.vue | 144 +++++++++++------- .../src/components/BezierExample.vue | 56 +++---- .../src/components/BezierExamplePane.vue | 58 ++++--- .../src/components/SubpathExample.vue | 28 +--- .../src/components/SubpathExamplePane.vue | 21 +-- .../bezier-rs-demos/src/utils/helpers.ts | 27 ---- .../other/bezier-rs-demos/src/utils/types.ts | 32 +++- .../other/bezier-rs-demos/wasm/src/bezier.rs | 32 ++-- 15 files changed, 341 insertions(+), 276 deletions(-) delete mode 100644 website/other/bezier-rs-demos/src/utils/helpers.ts diff --git a/editor/src/messages/tool/common_functionality/shape_editor.rs b/editor/src/messages/tool/common_functionality/shape_editor.rs index 254ffc71..25af562a 100644 --- a/editor/src/messages/tool/common_functionality/shape_editor.rs +++ b/editor/src/messages/tool/common_functionality/shape_editor.rs @@ -1,5 +1,6 @@ use crate::messages::prelude::*; +use bezier_rs::ComputeType; use graphene::layers::vector::consts::ManipulatorType; use graphene::layers::vector::manipulator_group::ManipulatorGroup; use graphene::layers::vector::manipulator_point::ManipulatorPoint; @@ -277,7 +278,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(t); + let layerspace = bezier.evaluate(ComputeType::Parametric(t)); let screenspace = transform.transform_point2(layerspace); let distance_squared = screenspace.distance_squared(position); diff --git a/libraries/bezier-rs/src/bezier/core.rs b/libraries/bezier-rs/src/bezier/core.rs index 3be7c2c4..a82f9c8a 100644 --- a/libraries/bezier-rs/src/bezier/core.rs +++ b/libraries/bezier-rs/src/bezier/core.rs @@ -203,6 +203,8 @@ impl Bezier { #[cfg(test)] mod tests { + use crate::utils::ComputeType; + use super::compare::compare_points; use super::*; @@ -213,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(0.5), p2)); + assert!(compare_points(bezier1.evaluate(ComputeType::Parametric(0.5)), p2)); let bezier2 = Bezier::quadratic_through_points(p1, p2, p3, Some(0.8)); - assert!(compare_points(bezier2.evaluate(0.8), p2)); + assert!(compare_points(bezier2.evaluate(ComputeType::Parametric(0.8)), p2)); let bezier3 = Bezier::quadratic_through_points(p1, p2, p3, Some(0.)); - assert!(compare_points(bezier3.evaluate(0.), p2)); + assert!(compare_points(bezier3.evaluate(ComputeType::Parametric(0.)), p2)); } #[test] @@ -229,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(0.3), p2)); + assert!(compare_points(bezier1.evaluate(ComputeType::Parametric(0.3)), p2)); let bezier2 = Bezier::cubic_through_points(p1, p2, p3, Some(0.8), Some(91.7)); - assert!(compare_points(bezier2.evaluate(0.8), p2)); + assert!(compare_points(bezier2.evaluate(ComputeType::Parametric(0.8)), p2)); let bezier3 = Bezier::cubic_through_points(p1, p2, p3, Some(0.), Some(91.7)); - assert!(compare_points(bezier3.evaluate(0.), p2)); + assert!(compare_points(bezier3.evaluate(ComputeType::Parametric(0.)), p2)); } } diff --git a/libraries/bezier-rs/src/bezier/lookup.rs b/libraries/bezier-rs/src/bezier/lookup.rs index 5181f95d..130b1d74 100644 --- a/libraries/bezier-rs/src/bezier/lookup.rs +++ b/libraries/bezier-rs/src/bezier/lookup.rs @@ -1,9 +1,11 @@ +use crate::utils::{f64_compare, ComputeType}; + use super::*; /// Functionality relating to looking up properties of the `Bezier` or points along the `Bezier`. impl Bezier { /// Calculate the point on the curve based on the `t`-value provided. - pub(crate) fn unrestricted_evaluate(&self, t: f64) -> DVec2 { + pub(crate) fn unrestricted_parametric_evaluate(&self, t: f64) -> DVec2 { // Basis code based off of pseudocode found here: . let t_squared = t * t; @@ -21,11 +23,48 @@ 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. /// Expects `t` to be within the inclusive range `[0, 1]`. - pub fn evaluate(&self, t: f64) -> DVec2 { - assert!((0.0..=1.).contains(&t)); - self.unrestricted_evaluate(t) + 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) + } + } } /// Return a selection of equidistant points on the bezier curve. @@ -36,7 +75,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(f64::from(t as i32) * ratio)) + steps_array.push(self.evaluate(ComputeType::Parametric(f64::from(t as i32) * ratio))) } steps_array @@ -123,7 +162,7 @@ impl Bezier { if step_index == 0 { distance = *table_distance; } else { - distance = point.distance(self.evaluate(iterator_t)); + distance = point.distance(self.evaluate(ComputeType::Parametric(iterator_t))); *table_distance = distance; } if distance < new_minimum_distance { @@ -173,23 +212,29 @@ mod tests { let p4 = DVec2::new(30., 21.); let bezier1 = Bezier::from_quadratic_dvec2(p1, p2, p3); - assert_eq!(bezier1.evaluate(0.5), DVec2::new(12.5, 6.25)); + assert_eq!(bezier1.evaluate(ComputeType::Parametric(0.5)), DVec2::new(12.5, 6.25)); let bezier2 = Bezier::from_cubic_dvec2(p1, p2, p3, p4); - assert_eq!(bezier2.evaluate(0.5), DVec2::new(16.5, 9.625)); + assert_eq!(bezier2.evaluate(ComputeType::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(0.5), bezier1.end()]); + assert_eq!(lookup_table1, vec![bezier1.start(), bezier1.evaluate(ComputeType::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)); assert_eq!( lookup_table2, - vec![bezier2.start(), bezier2.evaluate(0.25), bezier2.evaluate(0.5), bezier2.evaluate(0.75), bezier2.end()] + vec![ + bezier2.start(), + bezier2.evaluate(ComputeType::Parametric(0.25)), + bezier2.evaluate(ComputeType::Parametric(0.50)), + bezier2.evaluate(ComputeType::Parametric(0.75)), + bezier2.end() + ] ); } diff --git a/libraries/bezier-rs/src/bezier/solvers.rs b/libraries/bezier-rs/src/bezier/solvers.rs index 87f806f2..9866fd81 100644 --- a/libraries/bezier-rs/src/bezier/solvers.rs +++ b/libraries/bezier-rs/src/bezier/solvers.rs @@ -1,4 +1,5 @@ use super::*; +use crate::utils::ComputeType; use glam::DMat2; use std::ops::Range; @@ -52,7 +53,7 @@ impl Bezier { pub fn tangent(&self, t: f64) -> DVec2 { match self.handles { BezierHandles::Linear => self.end - self.start, - _ => self.derivative().unwrap().evaluate(t), + _ => self.derivative().unwrap().evaluate(ComputeType::Parametric(t)), } .normalize() } @@ -67,8 +68,8 @@ impl Bezier { pub fn curvature(&self, t: f64) -> f64 { let (d, dd) = match &self.derivative() { Some(first_derivative) => match first_derivative.derivative() { - Some(second_derivative) => (first_derivative.evaluate(t), second_derivative.evaluate(t)), - None => (first_derivative.evaluate(t), first_derivative.end - first_derivative.start), + 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), }, None => (self.end - self.start, DVec2::new(0., 0.)), }; @@ -128,7 +129,7 @@ impl Bezier { let extrema = self.local_extrema(); for t_values in extrema { for t in t_values { - let point = self.evaluate(t); + let point = self.evaluate(ComputeType::Parametric(t)); // Update bounding box if new min/max is found. endpoints_min = endpoints_min.min(point); endpoints_max = endpoints_max.max(point); @@ -276,7 +277,7 @@ impl Bezier { // Accept the t value if it is approximately in [0, 1] and if the corresponding coordinates are within the range of the linear line .filter(|&t| { utils::f64_approximately_in_range(t, 0., 1., MAX_ABSOLUTE_DIFFERENCE) - && utils::dvec2_approximately_in_range(self.unrestricted_evaluate(t), min, max, MAX_ABSOLUTE_DIFFERENCE).all() + && utils::dvec2_approximately_in_range(self.unrestricted_parametric_evaluate(t), min, max, MAX_ABSOLUTE_DIFFERENCE).all() }) // Ensure the returned value is within the correct range .map(|t| t.clamp(0., 1.)) @@ -348,7 +349,7 @@ mod tests { ]; assert_eq!(&de_casteljau_points, &expected_de_casteljau_points); - assert_eq!(expected_de_casteljau_points[3][0], bezier.evaluate(0.5)); + assert_eq!(expected_de_casteljau_points[3][0], bezier.evaluate(ComputeType::Parametric(0.5))); } #[test] @@ -564,12 +565,12 @@ mod tests { let line1 = Bezier::from_linear_coordinates(20., 60., 70., 60.); let intersections1 = bezier.intersections(&line1, None); assert!(intersections1.len() == 1); - assert!(compare_points(bezier.evaluate(intersections1[0]), DVec2::new(30., 60.))); + assert!(compare_points(bezier.evaluate(ComputeType::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); - assert!(compare_points(bezier.evaluate(intersections2[0]), DVec2::new(96., 96.))); + assert!(compare_points(bezier.evaluate(ComputeType::Parametric(intersections2[0])), DVec2::new(96., 96.))); } #[test] @@ -583,12 +584,12 @@ mod tests { let line1 = Bezier::from_linear_coordinates(20., 50., 40., 50.); let intersections1 = bezier.intersections(&line1, None); assert!(intersections1.len() == 1); - assert!(compare_points(bezier.evaluate(intersections1[0]), p1)); + assert!(compare_points(bezier.evaluate(ComputeType::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); - assert!(compare_points(bezier.evaluate(intersections2[0]), DVec2::new(47.77355, 47.77354))); + assert!(compare_points(bezier.evaluate(ComputeType::Parametric(intersections2[0])), DVec2::new(47.77355, 47.77354))); } #[test] @@ -603,14 +604,14 @@ mod tests { let line1 = Bezier::from_linear_coordinates(20., 30., 40., 30.); let intersections1 = bezier.intersections(&line1, None); assert!(intersections1.len() == 1); - assert!(compare_points(bezier.evaluate(intersections1[0]), p1)); + assert!(compare_points(bezier.evaluate(ComputeType::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); assert!(intersections2.len() == 2); - assert!(compare_points(bezier.evaluate(intersections2[0]), p1)); - assert!(compare_points(bezier.evaluate(intersections2[1]), DVec2::new(85.84, 85.84))); + 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))); } #[test] @@ -621,8 +622,8 @@ mod tests { let intersections = bezier1.intersections(&bezier2, None); let intersections2 = bezier2.intersections(&bezier1, None); assert!(compare_vec_of_points( - intersections.iter().map(|&t| bezier1.evaluate(t)).collect(), - intersections2.iter().map(|&t| bezier2.evaluate(t)).collect(), + intersections.iter().map(|&t| bezier1.evaluate(ComputeType::Parametric(t))).collect(), + intersections2.iter().map(|&t| bezier2.evaluate(ComputeType::Parametric(t))).collect(), 2. )); } @@ -632,8 +633,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(t[0])).collect(), - intersections.iter().map(|&t| bezier.evaluate(t[1])).collect(), + intersections.iter().map(|&t| bezier.evaluate(ComputeType::Parametric(t[0]))).collect(), + intersections.iter().map(|&t| bezier.evaluate(ComputeType::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 72fabe55..087b70b5 100644 --- a/libraries/bezier-rs/src/bezier/transform.rs +++ b/libraries/bezier-rs/src/bezier/transform.rs @@ -1,5 +1,5 @@ use super::*; -use crate::utils::f64_compare; +use crate::utils::{f64_compare, ComputeType}; use glam::DMat2; use std::f64::consts::PI; @@ -8,7 +8,7 @@ use std::f64::consts::PI; 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(t); + let split_point = self.evaluate(ComputeType::Parametric(t)); match self.handles { BezierHandles::Linear => [Bezier::from_linear_dvec2(self.start, split_point), Bezier::from_linear_dvec2(split_point, self.end)], @@ -53,7 +53,7 @@ impl Bezier { pub fn trim(&self, t1: f64, t2: f64) -> Bezier { // 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(t1); + let point = self.evaluate(ComputeType::Parametric(t1)); return match self.handles { BezierHandles::Linear => Bezier::from_linear_dvec2(point, point), BezierHandles::Quadratic { handle: _ } => Bezier::from_quadratic_dvec2(point, point, point), @@ -444,9 +444,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(low); - let p2 = self.evaluate(middle); - let p3 = self.evaluate(high); + let p1 = self.evaluate(ComputeType::Parametric(low)); + let p2 = self.evaluate(ComputeType::Parametric(middle)); + let p3 = self.evaluate(ComputeType::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 +486,8 @@ impl Bezier { }; // Use points in between low, middle, and high to evaluate how well the arc approximates the curve - let e1 = self.evaluate((low + middle) / 2.); - let e2 = self.evaluate((middle + high) / 2.); + let e1 = self.evaluate(ComputeType::Parametric((low + middle) / 2.)); + let e2 = self.evaluate(ComputeType::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,6 +537,8 @@ impl Bezier { #[cfg(test)] mod tests { + use crate::utils::ComputeType; + use super::compare::{compare_arcs, compare_vector_of_beziers}; use super::*; @@ -546,34 +548,34 @@ mod tests { let [part1, part2] = line.split(0.5); assert_eq!(part1.start(), line.start()); - assert_eq!(part1.end(), line.evaluate(0.5)); - assert_eq!(part1.evaluate(0.5), line.evaluate(0.25)); + 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!(part2.start(), line.evaluate(0.5)); + assert_eq!(part2.start(), line.evaluate(ComputeType::Parametric(0.5))); assert_eq!(part2.end(), line.end()); - assert_eq!(part2.evaluate(0.5), line.evaluate(0.75)); + assert_eq!(part2.evaluate(ComputeType::Parametric(0.5)), line.evaluate(ComputeType::Parametric(0.75))); let quad_bezier = Bezier::from_quadratic_coordinates(10., 10., 50., 50., 90., 10.); let [part3, part4] = quad_bezier.split(0.5); assert_eq!(part3.start(), quad_bezier.start()); - assert_eq!(part3.end(), quad_bezier.evaluate(0.5)); - assert_eq!(part3.evaluate(0.5), quad_bezier.evaluate(0.25)); + 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!(part4.start(), quad_bezier.evaluate(0.5)); + assert_eq!(part4.start(), quad_bezier.evaluate(ComputeType::Parametric(0.5))); assert_eq!(part4.end(), quad_bezier.end()); - assert_eq!(part4.evaluate(0.5), quad_bezier.evaluate(0.75)); + assert_eq!(part4.evaluate(ComputeType::Parametric(0.5)), quad_bezier.evaluate(ComputeType::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); assert_eq!(part5.start(), cubic_bezier.start()); - assert_eq!(part5.end(), cubic_bezier.evaluate(0.5)); - assert_eq!(part5.evaluate(0.5), cubic_bezier.evaluate(0.25)); + 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!(part6.start(), cubic_bezier.evaluate(0.5)); + assert_eq!(part6.start(), cubic_bezier.evaluate(ComputeType::Parametric(0.5))); assert_eq!(part6.end(), cubic_bezier.end()); - assert_eq!(part6.evaluate(0.5), cubic_bezier.evaluate(0.75)); + assert_eq!(part6.evaluate(ComputeType::Parametric(0.5)), cubic_bezier.evaluate(ComputeType::Parametric(0.75))); } #[test] @@ -611,23 +613,23 @@ mod tests { let line = Bezier::from_linear_coordinates(80., 80., 40., 40.); let trimmed1 = line.trim(0.25, 0.75); - assert_eq!(trimmed1.start(), line.evaluate(0.25)); - assert_eq!(trimmed1.end(), line.evaluate(0.75)); - assert_eq!(trimmed1.evaluate(0.5), line.evaluate(0.5)); + 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))); let quadratic_bezier = Bezier::from_quadratic_coordinates(80., 80., 40., 40., 70., 70.); let trimmed2 = quadratic_bezier.trim(0.25, 0.75); - assert_eq!(trimmed2.start(), quadratic_bezier.evaluate(0.25)); - assert_eq!(trimmed2.end(), quadratic_bezier.evaluate(0.75)); - assert_eq!(trimmed2.evaluate(0.5), quadratic_bezier.evaluate(0.5)); + 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))); let cubic_bezier = Bezier::from_cubic_coordinates(80., 80., 40., 40., 70., 70., 150., 150.); let trimmed3 = cubic_bezier.trim(0.25, 0.75); - assert_eq!(trimmed3.start(), cubic_bezier.evaluate(0.25)); - assert_eq!(trimmed3.end(), cubic_bezier.evaluate(0.75)); - assert_eq!(trimmed3.evaluate(0.5), cubic_bezier.evaluate(0.5)); + 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))); } #[test] @@ -741,16 +743,24 @@ mod tests { assert_eq!(outline.len(), 4); // Assert the first length-wise piece of the outline is 10 units from the line - assert!(f64_compare(outline[0].evaluate(0.25).distance(line.evaluate(0.25)), 10., MAX_ABSOLUTE_DIFFERENCE)); // f64 + assert!(f64_compare( + outline[0].evaluate(ComputeType::Parametric(0.25)).distance(line.evaluate(ComputeType::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(0.5).abs_diff_eq(line.end(), MAX_ABSOLUTE_DIFFERENCE)); + assert!(outline[1].evaluate(ComputeType::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(0.25).distance(line.evaluate(0.75)), 10., MAX_ABSOLUTE_DIFFERENCE)); // f64 + assert!(f64_compare( + outline[2].evaluate(ComputeType::Parametric(0.25)).distance(line.evaluate(ComputeType::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(0.5).abs_diff_eq(line.start(), MAX_ABSOLUTE_DIFFERENCE)); + assert!(outline[3].evaluate(ComputeType::Parametric(0.5)).abs_diff_eq(line.start(), MAX_ABSOLUTE_DIFFERENCE)); } #[test] @@ -767,9 +777,21 @@ mod tests { dbg!(scaled_bezier); // Assert the scaled bezier is 30 units from the line - assert!(f64_compare(scaled_bezier.evaluate(0.).distance(bezier.evaluate(0.)), 30., MAX_ABSOLUTE_DIFFERENCE)); - assert!(f64_compare(scaled_bezier.evaluate(1.).distance(bezier.evaluate(1.)), 30., MAX_ABSOLUTE_DIFFERENCE)); - assert!(f64_compare(scaled_bezier.evaluate(0.5).distance(bezier.evaluate(0.5)), 30., MAX_ABSOLUTE_DIFFERENCE)); + assert!(f64_compare( + scaled_bezier.evaluate(ComputeType::Parametric(0.)).distance(bezier.evaluate(ComputeType::Parametric(0.))), + 30., + MAX_ABSOLUTE_DIFFERENCE + )); + assert!(f64_compare( + scaled_bezier.evaluate(ComputeType::Parametric(1.)).distance(bezier.evaluate(ComputeType::Parametric(1.))), + 30., + MAX_ABSOLUTE_DIFFERENCE + )); + assert!(f64_compare( + scaled_bezier.evaluate(ComputeType::Parametric(0.5)).distance(bezier.evaluate(ComputeType::Parametric(0.5))), + 30., + MAX_ABSOLUTE_DIFFERENCE + )); } #[test] diff --git a/libraries/bezier-rs/src/lib.rs b/libraries/bezier-rs/src/lib.rs index 3b0d26b2..0da47242 100644 --- a/libraries/bezier-rs/src/lib.rs +++ b/libraries/bezier-rs/src/lib.rs @@ -7,3 +7,4 @@ mod utils; pub use bezier::*; pub use subpath::*; +pub use utils::ComputeType; diff --git a/libraries/bezier-rs/src/utils.rs b/libraries/bezier-rs/src/utils.rs index 694d5588..1cc115af 100644 --- a/libraries/bezier-rs/src/utils.rs +++ b/libraries/bezier-rs/src/utils.rs @@ -3,6 +3,13 @@ use crate::consts::{MAX_ABSOLUTE_DIFFERENCE, STRICT_MAX_ABSOLUTE_DIFFERENCE}; use glam::{BVec2, DMat2, DVec2}; use std::f64::consts::PI; +#[derive(Copy, Clone, PartialEq)] +pub enum ComputeType { + Parametric(f64), + Euclidean(f64), + EuclideanWithinError { t: f64, epsilon: f64 }, +} + /// Helper to perform the computation of a and c, where b is the provided point on the curve. /// Given the correct power of `t` and `(1-t)`, the computation is the same for quadratic and cubic cases. /// Relevant derivation and the definitions of a, b, and c can be found in [the projection identity section](https://pomax.github.io/bezierinfo/#abc) of Pomax's bezier curve primer. diff --git a/website/other/bezier-rs-demos/src/App.vue b/website/other/bezier-rs-demos/src/App.vue index 7b4055f9..607bab9d 100644 --- a/website/other/bezier-rs-demos/src/App.vue +++ b/website/other/bezier-rs-demos/src/App.vue @@ -1,23 +1,69 @@ + + - - diff --git a/website/other/bezier-rs-demos/src/components/BezierExample.vue b/website/other/bezier-rs-demos/src/components/BezierExample.vue index 536bc372..cf45a4d2 100644 --- a/website/other/bezier-rs-demos/src/components/BezierExample.vue +++ b/website/other/bezier-rs-demos/src/components/BezierExample.vue @@ -9,42 +9,31 @@ + + - - diff --git a/website/other/bezier-rs-demos/src/components/BezierExamplePane.vue b/website/other/bezier-rs-demos/src/components/BezierExamplePane.vue index 8aad4d64..df489362 100644 --- a/website/other/bezier-rs-demos/src/components/BezierExamplePane.vue +++ b/website/other/bezier-rs-demos/src/components/BezierExamplePane.vue @@ -1,6 +1,15 @@ + + - - diff --git a/website/other/bezier-rs-demos/src/components/SubpathExample.vue b/website/other/bezier-rs-demos/src/components/SubpathExample.vue index a0801371..a83afdd2 100644 --- a/website/other/bezier-rs-demos/src/components/SubpathExample.vue +++ b/website/other/bezier-rs-demos/src/components/SubpathExample.vue @@ -5,6 +5,8 @@ + + - - diff --git a/website/other/bezier-rs-demos/src/components/SubpathExamplePane.vue b/website/other/bezier-rs-demos/src/components/SubpathExamplePane.vue index 11337e4f..a57bfe12 100644 --- a/website/other/bezier-rs-demos/src/components/SubpathExamplePane.vue +++ b/website/other/bezier-rs-demos/src/components/SubpathExamplePane.vue @@ -9,6 +9,8 @@ + + - - diff --git a/website/other/bezier-rs-demos/src/utils/helpers.ts b/website/other/bezier-rs-demos/src/utils/helpers.ts deleted file mode 100644 index 939cba5b..00000000 --- a/website/other/bezier-rs-demos/src/utils/helpers.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { BezierCurveType, WasmBezierConstructorKey } from "@/utils/types"; - -export function getCurveType(numPoints: number): BezierCurveType { - switch (numPoints) { - case 2: - return BezierCurveType.Linear; - case 3: - return BezierCurveType.Quadratic; - case 4: - return BezierCurveType.Cubic; - default: - throw new Error("Invalid number of points for a bezier"); - } -} - -export function getConstructorKey(bezierCurveType: BezierCurveType): WasmBezierConstructorKey { - switch (bezierCurveType) { - case BezierCurveType.Linear: - return "new_linear"; - case BezierCurveType.Quadratic: - return "new_quadratic"; - case BezierCurveType.Cubic: - return "new_cubic"; - default: - throw new Error("Invalid value for a BezierCurveType"); - } -} diff --git a/website/other/bezier-rs-demos/src/utils/types.ts b/website/other/bezier-rs-demos/src/utils/types.ts index 6acbef64..b3bb05b5 100644 --- a/website/other/bezier-rs-demos/src/utils/types.ts +++ b/website/other/bezier-rs-demos/src/utils/types.ts @@ -8,13 +8,12 @@ export type WasmBezierManipulatorKey = "set_start" | "set_handle_start" | "set_h export type WasmSubpathInstance = InstanceType; export type WasmSubpathManipulatorKey = "set_anchor" | "set_in_handle" | "set_out_handle"; -export enum BezierCurveType { - Linear = "Linear", - Quadratic = "Quadratic", - Cubic = "Cubic", -} +export const BEZIER_CURVE_TYPE = ["Linear", "Quadratic", "Cubic"] as const; +export type BezierCurveType = typeof BEZIER_CURVE_TYPE[number]; -export type BezierCallback = (bezier: WasmBezierInstance, options: Record, mouseLocation?: [number, number]) => string; +export type ComputeType = "Euclidean" | "Parametric"; + +export type BezierCallback = (bezier: WasmBezierInstance, options: Record, mouseLocation?: [number, number], computeType?: ComputeType) => string; export type SubpathCallback = (subpath: WasmSubpathInstance) => string; export type ExampleOptions = { @@ -33,3 +32,24 @@ export type SliderOption = { variable: string; unit?: string | string[]; }; + +export function getCurveType(numPoints: number): BezierCurveType { + const mapping: Record = { + 2: "Linear", + 3: "Quadratic", + 4: "Cubic", + }; + + if (!(numPoints in mapping)) throw new Error("Invalid number of points for a bezier"); + + return mapping[numPoints]; +} + +export function getConstructorKey(bezierCurveType: BezierCurveType): WasmBezierConstructorKey { + const mapping: Record = { + Linear: "new_linear", + Quadratic: "new_quadratic", + Cubic: "new_cubic", + }; + return mapping[bezierCurveType]; +} diff --git a/website/other/bezier-rs-demos/wasm/src/bezier.rs b/website/other/bezier-rs-demos/wasm/src/bezier.rs index fde35dbb..535da93f 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, ProjectionOptions}; +use bezier_rs::{ArcStrategy, ArcsOptions, Bezier, ComputeType, ProjectionOptions}; use glam::DVec2; use serde::{Deserialize, Serialize}; use wasm_bindgen::prelude::*; @@ -132,10 +132,14 @@ 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) -> String { + pub fn evaluate(&self, t: f64, compute_type: String) -> String { let bezier = self.get_bezier_path(); - let point = &self.0.evaluate(t); - let content = format!("{bezier}{}", draw_circle(*point, 4., RED, 1.5, WHITE)); + 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 content = format!("{bezier}{}", draw_circle(point, 4., RED, 1.5, WHITE)); wrap_svg_tag(content) } @@ -173,7 +177,7 @@ impl WasmBezier { let bezier = self.get_bezier_path(); let tangent_point = self.0.tangent(t); - let intersection_point = self.0.evaluate(t); + let intersection_point = self.0.evaluate(ComputeType::Parametric(t)); let tangent_end = intersection_point + tangent_point * SCALE_UNIT_VECTOR_FACTOR; let content = format!( @@ -189,7 +193,7 @@ impl WasmBezier { let bezier = self.get_bezier_path(); let normal_point = self.0.normal(t); - let intersection_point = self.0.evaluate(t); + let intersection_point = self.0.evaluate(ComputeType::Parametric(t)); let normal_end = intersection_point + normal_point * SCALE_UNIT_VECTOR_FACTOR; let content = format!( @@ -205,7 +209,7 @@ impl WasmBezier { let bezier = self.get_bezier_path(); let radius = 1. / self.0.curvature(t); let normal_point = self.0.normal(t); - let intersection_point = self.0.evaluate(t); + let intersection_point = self.0.evaluate(ComputeType::Parametric(t)); let curvature_center = intersection_point + normal_point * radius; @@ -269,7 +273,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(projected_t_value); + let projected_point = self.0.evaluate(ComputeType::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 +289,7 @@ impl WasmBezier { .zip([RED, GREEN]) .flat_map(|(t_value_list, color)| { t_value_list.iter().map(|&t_value| { - let point = self.0.evaluate(t_value); + let point = self.0.evaluate(ComputeType::Parametric(t_value)); draw_circle(point, 3., color, 1.5, WHITE) }) }) @@ -320,7 +324,7 @@ impl WasmBezier { let circles: String = inflections .iter() .map(|&t_value| { - let point = self.0.evaluate(t_value); + let point = self.0.evaluate(ComputeType::Parametric(t_value)); draw_circle(point, 3., RED, 1.5, WHITE) }) .fold("".to_string(), |acc, circle| acc + &circle); @@ -413,7 +417,7 @@ impl WasmBezier { .intersect(&line, None) .iter() .map(|intersection_t| { - let point = &self.0.evaluate(*intersection_t); + let point = &self.0.evaluate(ComputeType::Parametric(*intersection_t)); draw_circle(*point, 4., RED, 1.5, WHITE) }) .fold(String::new(), |acc, item| format!("{acc}{item}")); @@ -433,7 +437,7 @@ impl WasmBezier { .intersect(&quadratic, Some(error)) .iter() .map(|intersection_t| { - let point = &self.0.evaluate(*intersection_t); + let point = &self.0.evaluate(ComputeType::Parametric(*intersection_t)); draw_circle(*point, 4., RED, 1.5, WHITE) }) .fold(String::new(), |acc, item| format!("{acc}{item}")); @@ -453,7 +457,7 @@ impl WasmBezier { .intersect(&cubic, Some(error)) .iter() .map(|intersection_t| { - let point = &self.0.evaluate(*intersection_t); + let point = &self.0.evaluate(ComputeType::Parametric(*intersection_t)); draw_circle(*point, 4., RED, 1.5, WHITE) }) .fold(String::new(), |acc, item| format!("{acc}{item}")); @@ -469,7 +473,7 @@ impl WasmBezier { .self_intersections(Some(error)) .iter() .map(|intersection_t| { - let point = &self.0.evaluate(intersection_t[0]); + let point = &self.0.evaluate(ComputeType::Parametric(intersection_t[0])); draw_circle(*point, 4., RED, 1.5, WHITE) }) .fold(bezier_curve_svg, |acc, item| format!("{acc}{item}"));