diff --git a/libraries/bezier-rs/src/subpath/manipulators.rs b/libraries/bezier-rs/src/subpath/manipulators.rs new file mode 100644 index 000000000..67e7a541e --- /dev/null +++ b/libraries/bezier-rs/src/subpath/manipulators.rs @@ -0,0 +1,149 @@ +use super::*; +use crate::consts::MAX_ABSOLUTE_DIFFERENCE; +use crate::utils::f64_compare; +use crate::ComputeType; + +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) { + match t { + ComputeType::Parametric(t) => { + assert!((0.0..=1.).contains(&t)); + + let number_of_curves = self.len_segments() as f64; + let scaled_t = t * number_of_curves; + + let target_curve_index = scaled_t.floor() as i32; + let target_curve_t = scaled_t % 1.; + + if f64_compare(target_curve_t, 0., MAX_ABSOLUTE_DIFFERENCE) || f64_compare(target_curve_t, 1., MAX_ABSOLUTE_DIFFERENCE) { + return; + } + + // The only case where `curve` would be `None` is if the provided argument was 1 + // But the above if case would catch that, since `target_curve_t` would be 0. + let curve = self.iter().nth(target_curve_index as usize).unwrap(); + + let [first, second] = curve.split(target_curve_t); + let new_group = ManipulatorGroup { + anchor: first.end(), + in_handle: first.handle_end(), + out_handle: second.handle_start(), + }; + let number_of_groups = self.manipulator_groups.len() + 1; + self.manipulator_groups.insert((target_curve_index as usize) + 1, new_group); + self.manipulator_groups[(target_curve_index as usize) % number_of_groups].out_handle = first.handle_start(); + self.manipulator_groups[((target_curve_index as usize) + 2) % number_of_groups].in_handle = second.handle_end(); + } + // TODO: change this implementation to Euclidean compute + ComputeType::Euclidean(_t) => {} + ComputeType::EuclideanWithinError { t: _, epsilon: _ } => todo!(), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use glam::DVec2; + + fn set_up_open_subpath() -> Subpath { + let start = DVec2::new(20., 30.); + let middle1 = DVec2::new(80., 90.); + let middle2 = DVec2::new(100., 100.); + let end = DVec2::new(60., 45.); + + let handle1 = DVec2::new(75., 85.); + let handle2 = DVec2::new(40., 30.); + let handle3 = DVec2::new(10., 10.); + + return Subpath::new( + vec![ + ManipulatorGroup { + anchor: start, + in_handle: None, + out_handle: Some(handle1), + }, + ManipulatorGroup { + anchor: middle1, + in_handle: None, + out_handle: Some(handle2), + }, + ManipulatorGroup { + anchor: middle2, + in_handle: None, + out_handle: None, + }, + ManipulatorGroup { + anchor: end, + in_handle: None, + out_handle: Some(handle3), + }, + ], + false, + ); + } + + fn set_up_closed_subpath() -> Subpath { + let mut subpath = set_up_open_subpath(); + subpath.closed = true; + subpath + } + + #[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)); + 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()); + } + + #[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)); + 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()); + } + + #[test] + 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)); + assert_eq!(subpath.manipulator_groups[3].anchor, location); + assert_eq!(subpath.manipulator_groups.len(), 5); + assert_eq!(subpath.len_segments(), 4); + } + + #[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)); + 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()); + assert!(subpath.closed); + } + + #[test] + 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.)); + 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/mod.rs b/libraries/bezier-rs/src/subpath/mod.rs index 79a3155eb..0211bb1e0 100644 --- a/libraries/bezier-rs/src/subpath/mod.rs +++ b/libraries/bezier-rs/src/subpath/mod.rs @@ -1,5 +1,6 @@ mod core; mod lookup; +mod manipulators; mod solvers; mod structs; pub use structs::*; @@ -9,6 +10,7 @@ use crate::Bezier; use std::ops::{Index, IndexMut}; /// Structure used to represent a path composed of [Bezier] curves. +#[derive(Clone, PartialEq)] pub struct Subpath { manipulator_groups: Vec, closed: bool, diff --git a/libraries/bezier-rs/src/subpath/structs.rs b/libraries/bezier-rs/src/subpath/structs.rs index 1ad55dac0..4ee126040 100644 --- a/libraries/bezier-rs/src/subpath/structs.rs +++ b/libraries/bezier-rs/src/subpath/structs.rs @@ -1,8 +1,24 @@ use glam::DVec2; +use std::fmt::{Debug, Formatter, Result}; /// Structure used to represent a single anchor with up to two optional associated handles along a `Subpath` +#[derive(Copy, Clone, PartialEq)] pub struct ManipulatorGroup { pub anchor: DVec2, pub in_handle: Option, pub out_handle: Option, } + +impl Debug for ManipulatorGroup { + fn fmt(&self, f: &mut Formatter<'_>) -> Result { + if self.in_handle.is_some() && self.out_handle.is_some() { + write!(f, "anchor: {}, in: {}, out: {}", self.anchor, self.in_handle.unwrap(), self.out_handle.unwrap()) + } else if self.in_handle.is_some() { + write!(f, "anchor: {}, in: {}, out: n/a", self.anchor, self.in_handle.unwrap()) + } else if self.out_handle.is_some() { + write!(f, "anchor: {}, in: n/a, out: {}", self.anchor, self.out_handle.unwrap()) + } else { + write!(f, "anchor: {}, in: n/a, out: n/a", self.anchor) + } + } +} diff --git a/website/other/bezier-rs-demos/src/App.vue b/website/other/bezier-rs-demos/src/App.vue index 76760398d..0cde84950 100644 --- a/website/other/bezier-rs-demos/src/App.vue +++ b/website/other/bezier-rs-demos/src/App.vue @@ -601,6 +601,13 @@ export default defineComponent({ name: "Constructor", callback: (subpath: WasmSubpathInstance): string => subpath.to_svg(), }, + { + name: "Insert", + callback: (subpath: WasmSubpathInstance, options: Record, _: undefined, computeType: ComputeType): string => subpath.insert(options.computeArgument, computeType), + sliderOptions: [{ ...tSliderOptions, variable: "computeArgument" }], + // TODO: Uncomment this after implementing the Euclidean version + // chooseComputeType: true, + }, { name: "Length", callback: (subpath: WasmSubpathInstance): string => subpath.length(), diff --git a/website/other/bezier-rs-demos/wasm/src/subpath.rs b/website/other/bezier-rs-demos/wasm/src/subpath.rs index 7b104a9f4..8108f3da2 100644 --- a/website/other/bezier-rs-demos/wasm/src/subpath.rs +++ b/website/other/bezier-rs-demos/wasm/src/subpath.rs @@ -53,6 +53,26 @@ impl WasmSubpath { subpath_svg } + pub fn insert(&self, t: f64, compute_type: String) -> String { + let mut subpath = self.0.clone(); + let point = match compute_type.as_str() { + "Euclidean" => { + let parameter = ComputeType::Euclidean(t); + subpath.insert(parameter); + self.0.evaluate(parameter) + } + "Parametric" => { + let parameter = ComputeType::Parametric(t); + subpath.insert(parameter); + self.0.evaluate(parameter) + } + _ => panic!("Unexpected ComputeType string: '{}'", compute_type), + }; + let point_text = draw_circle(point, 4., RED, 1.5, WHITE); + + wrap_svg_tag(format!("{}{}", WasmSubpath(subpath).to_default_svg(), point_text)) + } + pub fn length(&self) -> String { let length_text = draw_text(format!("Length: {:.2}", self.0.length(None)), 5., 193., BLACK); wrap_svg_tag(format!("{}{}", self.to_default_svg(), length_text))