diff --git a/editor/src/messages/tool/common_functionality/shape_editor.rs b/editor/src/messages/tool/common_functionality/shape_editor.rs index 6407c8d6..1034fae1 100644 --- a/editor/src/messages/tool/common_functionality/shape_editor.rs +++ b/editor/src/messages/tool/common_functionality/shape_editor.rs @@ -375,7 +375,7 @@ impl ShapeState { for subpath in &vector_data.subpaths { for (manipulator_index, bezier) in subpath.iter().enumerate() { - let t = bezier.project(layer_pos, projection_options); + let t = bezier.project(layer_pos, Some(projection_options)); let layerspace = bezier.evaluate(TValue::Parametric(t)); let screenspace = transform.transform_point2(layerspace); diff --git a/libraries/bezier-rs/src/bezier/lookup.rs b/libraries/bezier-rs/src/bezier/lookup.rs index 352c162d..379abcb3 100644 --- a/libraries/bezier-rs/src/bezier/lookup.rs +++ b/libraries/bezier-rs/src/bezier/lookup.rs @@ -1,4 +1,4 @@ -use crate::utils::{f64_compare, TValue}; +use crate::utils::{f64_compare, TValue, TValueType}; use super::*; @@ -74,16 +74,19 @@ impl Bezier { /// 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) -> Vec { - let steps_unwrapped = steps.unwrap_or(DEFAULT_LUT_STEP_SIZE); - let ratio: f64 = 1. / (steps_unwrapped as f64); - let mut steps_array = Vec::with_capacity(steps_unwrapped + 1); + pub fn compute_lookup_table(&self, steps: Option, tvalue_type: Option) -> Vec { + let steps = steps.unwrap_or(DEFAULT_LUT_STEP_SIZE); + let tvalue_type = tvalue_type.unwrap_or(TValueType::Parametric); - for t in 0..steps_unwrapped + 1 { - steps_array.push(self.evaluate(TValue::Parametric(f64::from(t as i32) * ratio))) - } - - steps_array + (0..=steps) + .map(|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) + }) + .collect() } /// Return an approximation of the length of the bezier curve. @@ -97,7 +100,7 @@ impl Bezier { // We will use an approximate approach where we split the curve into many subdivisions // and calculate the euclidean distance between the two endpoints of the subdivision - let lookup_table = self.compute_lookup_table(Some(num_subdivisions.unwrap_or(DEFAULT_LENGTH_SUBDIVISIONS))); + let lookup_table = self.compute_lookup_table(Some(num_subdivisions.unwrap_or(DEFAULT_LENGTH_SUBDIVISIONS)), Some(TValueType::Parametric)); let mut approx_curve_length = 0.; let mut previous_point = lookup_table[0]; // Calculate approximate distance between subdivision @@ -114,9 +117,10 @@ impl Bezier { } /// 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. + /// Uses a searching algorithm akin to binary search that can be customized using the optional [ProjectionOptions] struct. /// - pub fn project(&self, point: DVec2, options: ProjectionOptions) -> f64 { + pub fn project(&self, point: DVec2, options: Option) -> f64 { + let options = options.unwrap_or_default(); let ProjectionOptions { lut_size, convergence_epsilon, @@ -126,7 +130,7 @@ impl Bezier { // TODO: Consider optimizations from precomputing useful values, or using the GPU // First find the closest point from the results of a lookup table - let lut = self.compute_lookup_table(Some(lut_size)); + let lut = self.compute_lookup_table(Some(lut_size), Some(TValueType::Parametric)); let (minimum_position, minimum_distance) = utils::get_closest_point_in_lut(&lut, point); // Get the t values to the left and right of the closest result in the lookup table @@ -228,11 +232,11 @@ mod tests { #[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)); + let lookup_table1 = bezier1.compute_lookup_table(Some(2), Some(TValueType::Parametric)); 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)); + let lookup_table2 = bezier2.compute_lookup_table(Some(4), Some(TValueType::Parametric)); assert_eq!( lookup_table2, vec![ @@ -264,13 +268,11 @@ mod tests { #[test] fn test_project() { - let project_options = ProjectionOptions::default(); - let bezier1 = Bezier::from_cubic_coordinates(4., 4., 23., 45., 10., 30., 56., 90.); - assert_eq!(bezier1.project(DVec2::ZERO, project_options), 0.); - assert_eq!(bezier1.project(DVec2::new(100., 100.), project_options), 1.); + assert_eq!(bezier1.project(DVec2::ZERO, None), 0.); + assert_eq!(bezier1.project(DVec2::new(100., 100.), None), 1.); let bezier2 = Bezier::from_quadratic_coordinates(0., 0., 0., 100., 100., 100.); - assert_eq!(bezier2.project(DVec2::new(100., 0.), project_options), 0.); + assert_eq!(bezier2.project(DVec2::new(100., 0.), None), 0.); } } diff --git a/libraries/bezier-rs/src/bezier/transform.rs b/libraries/bezier-rs/src/bezier/transform.rs index 4df23d0d..047b1bfa 100644 --- a/libraries/bezier-rs/src/bezier/transform.rs +++ b/libraries/bezier-rs/src/bezier/transform.rs @@ -315,12 +315,12 @@ impl Bezier { BezierHandles::Linear => Bezier::from_linear_dvec2(transformed_start, transformed_end), BezierHandles::Quadratic { handle: _ } => unreachable!(), BezierHandles::Cubic { handle_start, handle_end } => { - let handle_start_closest_t = intermediate.project(handle_start, ProjectionOptions::default()); + let handle_start_closest_t = intermediate.project(handle_start, None); 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, intermediate.normal(TValue::Parametric(handle_start_closest_t)), false, handle_start_scale_distance); - let handle_end_closest_t = intermediate.project(handle_start, ProjectionOptions::default()); + let handle_end_closest_t = intermediate.project(handle_start, None); 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, intermediate.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) @@ -810,7 +810,7 @@ mod tests { .iter() .map(|t| { let offset_point = offset_segment.evaluate(TValue::Parametric(*t)); - let closest_point_t = bezier.project(offset_point, ProjectionOptions::default()); + let closest_point_t = bezier.project(offset_point, None); let closest_point = bezier.evaluate(TValue::Parametric(closest_point_t)); let actual_distance = offset_point.distance(closest_point); diff --git a/libraries/bezier-rs/src/lib.rs b/libraries/bezier-rs/src/lib.rs index cc74ff03..3d44f643 100644 --- a/libraries/bezier-rs/src/lib.rs +++ b/libraries/bezier-rs/src/lib.rs @@ -8,4 +8,4 @@ mod utils; pub use bezier::*; pub use subpath::*; -pub use utils::{Cap, Join, SubpathTValue, TValue}; +pub use utils::{Cap, Join, SubpathTValue, TValue, TValueType}; diff --git a/libraries/bezier-rs/src/subpath/lookup.rs b/libraries/bezier-rs/src/subpath/lookup.rs index 40dd319d..f1e3a051 100644 --- a/libraries/bezier-rs/src/subpath/lookup.rs +++ b/libraries/bezier-rs/src/subpath/lookup.rs @@ -1,11 +1,29 @@ use super::*; -use crate::consts::DEFAULT_EUCLIDEAN_ERROR_BOUND; -use crate::utils::{SubpathTValue, TValue}; +use crate::consts::{DEFAULT_EUCLIDEAN_ERROR_BOUND, DEFAULT_LUT_STEP_SIZE}; +use crate::utils::{SubpathTValue, TValue, TValueType}; use crate::ProjectionOptions; use glam::DVec2; /// Functionality relating to looking up properties of the `Subpath` or points along the `Subpath`. impl Subpath { + /// 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) -> Vec { + let steps = steps.unwrap_or(DEFAULT_LUT_STEP_SIZE); + let tvalue_type = tvalue_type.unwrap_or(TValueType::Parametric); + + (0..=steps) + .map(|t| { + let tvalue = match tvalue_type { + TValueType::Parametric => SubpathTValue::GlobalParametric(t as f64 / steps as f64), + TValueType::Euclidean => SubpathTValue::GlobalEuclidean(t as f64 / steps as f64), + }; + self.evaluate(tvalue) + }) + .collect() + } + /// Return the sum of the approximation of the length of each `Bezier` curve along the `Subpath`. /// - `num_subdivisions` - Number of subdivisions used to approximate the curve. The default value is `1000`. /// @@ -80,7 +98,7 @@ impl Subpath { /// Returns the segment index and `t` value that corresponds to the closest point on the curve to the provided point. /// Uses a searching algorithm akin to binary search that can be customized using the [ProjectionOptions] structure. /// - pub fn project(&self, point: DVec2, options: ProjectionOptions) -> Option<(usize, f64)> { + pub fn project(&self, point: DVec2, options: Option) -> Option<(usize, f64)> { if self.is_empty() { return None; } diff --git a/libraries/bezier-rs/src/utils.rs b/libraries/bezier-rs/src/utils.rs index 8567de74..62b1c5dc 100644 --- a/libraries/bezier-rs/src/utils.rs +++ b/libraries/bezier-rs/src/utils.rs @@ -19,6 +19,12 @@ pub enum TValue { EuclideanWithinError { t: f64, error: f64 }, } +#[derive(Copy, Clone, PartialEq)] +pub enum TValueType { + Parametric, + Euclidean, +} + #[derive(Copy, Clone, PartialEq)] pub enum SubpathTValue { Parametric { segment_index: usize, t: f64 }, 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 602414fd..4c7849aa 100644 --- a/website/other/bezier-rs-demos/src/features/bezier-features.ts +++ b/website/other/bezier-rs-demos/src/features/bezier-features.ts @@ -76,10 +76,11 @@ const bezierFeatures = { }, "lookup-table": { name: "Lookup Table", - callback: (bezier: WasmBezierInstance, options: Record): string => bezier.compute_lookup_table(options.steps), + callback: (bezier: WasmBezierInstance, options: Record, _: undefined): string => bezier.compute_lookup_table(options.steps, BEZIER_T_VALUE_VARIANTS[options.TVariant]), demoOptions: { Quadratic: { inputOptions: [ + bezierTValueVariantOptions, { min: 2, max: 15, 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 aab782ca..b64f05a4 100644 --- a/website/other/bezier-rs-demos/src/features/subpath-features.ts +++ b/website/other/bezier-rs-demos/src/features/subpath-features.ts @@ -20,6 +20,20 @@ const subpathFeatures = { callback: (subpath: WasmSubpathInstance, options: Record, _: undefined): string => subpath.evaluate(options.t, SUBPATH_T_VALUE_VARIANTS[options.TVariant]), inputOptions: [subpathTValueVariantOptions, tSliderOptions], }, + "lookup-table": { + name: "Lookup Table", + callback: (subpath: WasmSubpathInstance, options: Record, _: undefined): string => subpath.compute_lookup_table(options.steps, SUBPATH_T_VALUE_VARIANTS[options.TVariant]), + inputOptions: [ + subpathTValueVariantOptions, + { + min: 2, + max: 30, + step: 1, + default: 5, + variable: "steps", + }, + ], + }, project: { name: "Project", callback: (subpath: WasmSubpathInstance, _: Record, mouseLocation?: [number, number]): string => diff --git a/website/other/bezier-rs-demos/wasm/src/bezier.rs b/website/other/bezier-rs-demos/wasm/src/bezier.rs index 6cb49b86..68c0c1e5 100644 --- a/website/other/bezier-rs-demos/wasm/src/bezier.rs +++ b/website/other/bezier-rs-demos/wasm/src/bezier.rs @@ -1,7 +1,7 @@ use crate::svg_drawing::*; use crate::utils::parse_cap; -use bezier_rs::{ArcStrategy, ArcsOptions, Bezier, Identifier, ProjectionOptions, TValue}; +use bezier_rs::{ArcStrategy, ArcsOptions, Bezier, Identifier, TValue, TValueType}; use glam::DVec2; use serde::{Deserialize, Serialize}; use wasm_bindgen::prelude::*; @@ -156,9 +156,14 @@ impl WasmBezier { wrap_svg_tag(content) } - pub fn compute_lookup_table(&self, steps: usize) -> String { + pub fn compute_lookup_table(&self, steps: usize, t_variant: String) -> String { let bezier = self.get_bezier_path(); - let table_values: Vec = self.0.compute_lookup_table(Some(steps)); + let tvalue_type = match t_variant.as_str() { + "Parametric" => TValueType::Parametric, + "Euclidean" => TValueType::Euclidean, + _ => panic!("Unexpected TValue string: '{}'", t_variant), + }; + let table_values: Vec = self.0.compute_lookup_table(Some(steps), Some(tvalue_type)); let circles: String = table_values .iter() .map(|point| draw_circle(*point, 3., RED, 1.5, WHITE)) @@ -287,7 +292,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_t_value = self.0.project(DVec2::new(x, y), None); let projected_point = self.0.evaluate(TValue::Parametric(projected_t_value)); let bezier = self.get_bezier_path(); diff --git a/website/other/bezier-rs-demos/wasm/src/subpath.rs b/website/other/bezier-rs-demos/wasm/src/subpath.rs index 237c9979..3706f8fd 100644 --- a/website/other/bezier-rs-demos/wasm/src/subpath.rs +++ b/website/other/bezier-rs-demos/wasm/src/subpath.rs @@ -1,7 +1,7 @@ use crate::svg_drawing::*; use crate::utils::{parse_cap, parse_join}; -use bezier_rs::{Bezier, ManipulatorGroup, ProjectionOptions, Subpath, SubpathTValue}; +use bezier_rs::{Bezier, ManipulatorGroup, Subpath, SubpathTValue, TValueType}; use glam::DVec2; use std::fmt::Write; @@ -99,6 +99,22 @@ impl WasmSubpath { wrap_svg_tag(format!("{}{}", self.to_default_svg(), point_text)) } + pub fn compute_lookup_table(&self, steps: usize, t_variant: String) -> String { + let subpath = self.to_default_svg(); + let tvalue_type = match t_variant.as_str() { + "GlobalParametric" => TValueType::Parametric, + "GlobalEuclidean" => TValueType::Euclidean, + _ => panic!("Unexpected TValue string: '{}'", t_variant), + }; + let table_values: Vec = self.0.compute_lookup_table(Some(steps), Some(tvalue_type)); + let circles: String = table_values + .iter() + .map(|point| draw_circle(*point, 3., RED, 1.5, WHITE)) + .fold("".to_string(), |acc, circle| acc + &circle); + let content = format!("{subpath}{circles}"); + wrap_svg_tag(content) + } + pub fn tangent(&self, t: f64, t_variant: String) -> String { let t = parse_t_variant(&t_variant, t); @@ -204,7 +220,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 (segment_index, projected_t) = self.0.project(DVec2::new(x, y), None).unwrap(); let projected_point = self.0.evaluate(SubpathTValue::Parametric { segment_index, t: projected_t }); let subpath_svg = self.to_default_svg(); diff --git a/website/other/bezier-rs-demos/wasm/src/svg_drawing.rs b/website/other/bezier-rs-demos/wasm/src/svg_drawing.rs index f2e25877..d33bb505 100644 --- a/website/other/bezier-rs-demos/wasm/src/svg_drawing.rs +++ b/website/other/bezier-rs-demos/wasm/src/svg_drawing.rs @@ -10,7 +10,7 @@ pub const WHITE: &str = "white"; pub const GRAY: &str = "gray"; pub const RED: &str = "red"; pub const ORANGE: &str = "orange"; -pub const PINK: &str = "pink"; +// pub const PINK: &str = "pink"; pub const GREEN: &str = "green"; pub const NONE: &str = "none";