From 8d3daeae787b50aa08635a95e9a60a480e15c409 Mon Sep 17 00:00:00 2001 From: Hannah Li Date: Sun, 26 Feb 2023 18:41:11 -0500 Subject: [PATCH] Bezier-rs: Add trim for Subpath (#1006) * Move compare.rs * Update traits for Subpath and ManipulatorGroup * Implement trim * UI adjustments and more tests * Add reverse, refactor code, rename variables * Improve comments * Comment nits * Address comments * Update trim behavior * Update doc comment for trim --------- Co-authored-by: Keavon Chambers --- libraries/bezier-rs/src/bezier/core.rs | 5 +- libraries/bezier-rs/src/bezier/mod.rs | 12 +- libraries/bezier-rs/src/bezier/solvers.rs | 2 +- libraries/bezier-rs/src/bezier/transform.rs | 41 +- .../bezier-rs/src/{bezier => }/compare.rs | 8 +- libraries/bezier-rs/src/lib.rs | 2 + libraries/bezier-rs/src/subpath/core.rs | 2 +- .../bezier-rs/src/subpath/manipulators.rs | 5 + libraries/bezier-rs/src/subpath/mod.rs | 28 +- libraries/bezier-rs/src/subpath/structs.rs | 14 +- libraries/bezier-rs/src/subpath/transform.rs | 492 +++++++++++++++++- .../src/features/subpath-features.ts | 9 + .../other/bezier-rs-demos/wasm/src/subpath.rs | 17 + 13 files changed, 572 insertions(+), 65 deletions(-) rename libraries/bezier-rs/src/{bezier => }/compare.rs (77%) diff --git a/libraries/bezier-rs/src/bezier/core.rs b/libraries/bezier-rs/src/bezier/core.rs index 82486001..9fb1c291 100644 --- a/libraries/bezier-rs/src/bezier/core.rs +++ b/libraries/bezier-rs/src/bezier/core.rs @@ -205,10 +205,9 @@ impl Bezier { #[cfg(test)] mod tests { - use crate::utils::TValue; - - use super::compare::compare_points; use super::*; + use crate::compare::compare_points; + use crate::utils::TValue; #[test] fn test_quadratic_from_points() { diff --git a/libraries/bezier-rs/src/bezier/mod.rs b/libraries/bezier-rs/src/bezier/mod.rs index 2240f412..1efa693b 100644 --- a/libraries/bezier-rs/src/bezier/mod.rs +++ b/libraries/bezier-rs/src/bezier/mod.rs @@ -1,6 +1,3 @@ -#[cfg(test)] -pub(super) mod compare; - mod core; mod lookup; mod manipulators; @@ -47,6 +44,13 @@ pub struct Bezier { impl Debug for Bezier { fn fmt(&self, f: &mut Formatter<'_>) -> Result { - write!(f, "{:?}", self.get_points().collect::>()) + let mut debug_struct = f.debug_struct("Bezier"); + let mut debug_struct_ref = debug_struct.field("start", &self.start); + debug_struct_ref = match self.handles { + BezierHandles::Linear => debug_struct_ref, + BezierHandles::Quadratic { handle } => debug_struct_ref.field("handle", &handle), + BezierHandles::Cubic { handle_start, handle_end } => debug_struct_ref.field("handle_start", &handle_start).field("handle_end", &handle_end), + }; + debug_struct_ref.field("end", &self.end).finish() } } diff --git a/libraries/bezier-rs/src/bezier/solvers.rs b/libraries/bezier-rs/src/bezier/solvers.rs index 4100ae3b..c64ad02d 100644 --- a/libraries/bezier-rs/src/bezier/solvers.rs +++ b/libraries/bezier-rs/src/bezier/solvers.rs @@ -394,8 +394,8 @@ impl Bezier { #[cfg(test)] mod tests { - use super::compare::{compare_f64s, compare_points, compare_vec_of_points}; use super::*; + use crate::compare::{compare_f64s, compare_points, compare_vec_of_points}; #[test] fn test_de_casteljau_points() { diff --git a/libraries/bezier-rs/src/bezier/transform.rs b/libraries/bezier-rs/src/bezier/transform.rs index 7c3a8b59..599ff202 100644 --- a/libraries/bezier-rs/src/bezier/transform.rs +++ b/libraries/bezier-rs/src/bezier/transform.rs @@ -51,11 +51,11 @@ impl 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`. + /// Returns the Bezier curve representing the sub-curve between the two provided points. + /// It will start at the point corresponding to the smaller of `t1` and `t2`, and end at the point corresponding to the larger of `t1` and `t2`. /// pub fn trim(&self, t1: TValue, t2: TValue) -> Bezier { - let (t1, t2) = (self.t_value_to_parametric(t1), self.t_value_to_parametric(t2)); + let (mut t1, mut 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(TValue::Parametric(t1)); @@ -64,25 +64,13 @@ impl Bezier { BezierHandles::Quadratic { handle: _ } => Bezier::from_quadratic_dvec2(point, point, point), BezierHandles::Cubic { handle_start: _, handle_end: _ } => Bezier::from_cubic_dvec2(point, point, point, point), }; + } else if t1 > t2 { + (t1, t2) = (t2, t1) } - // 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(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 - // Also cover the `t1` == t2 case where there would otherwise be a divide by 0 - (t2 - t1) / (1. - t1) - } else { - // Case where we took the split from the beginning to `t1` - t2 / t1 - }; - let result = bezier_starting_at_t1.split(TValue::Parametric(adjusted_t2))[t2_split_side]; - if t2 < t1 { - return result.reverse(); - } - result + let bezier_ending_at_t2 = self.split(TValue::Parametric(t2))[0]; + // Adjust the ratio `t1` to its corresponding value on the new curve that was split on `t2` + let adjusted_t1 = t1 / t2; + bezier_ending_at_t2.split(TValue::Parametric(adjusted_t1))[1] } /// Returns a Bezier curve that results from applying the transformation function to each point in the Bezier. @@ -549,10 +537,9 @@ impl Bezier { #[cfg(test)] mod tests { - use crate::utils::TValue; - - use super::compare::{compare_arcs, compare_vector_of_beziers}; use super::*; + use crate::compare::{compare_arcs, compare_vector_of_beziers}; + use crate::utils::TValue; #[test] fn test_split() { @@ -639,7 +626,7 @@ mod tests { let cubic_bezier = Bezier::from_cubic_coordinates(80., 80., 40., 40., 70., 70., 150., 150.); let trimmed3 = cubic_bezier.trim(TValue::Parametric(0.25), TValue::Parametric(0.75)); - assert_eq!(trimmed3.start(), cubic_bezier.evaluate(TValue::Parametric(0.25))); + assert!(trimmed3.start().abs_diff_eq(cubic_bezier.evaluate(TValue::Parametric(0.25)), MAX_ABSOLUTE_DIFFERENCE)); 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))); } @@ -649,13 +636,13 @@ mod tests { // 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(TValue::Parametric(0.25), TValue::Parametric(0.75)); - let trim2 = bezier_quadratic.trim(TValue::Parametric(0.75), TValue::Parametric(0.25)).reverse(); + let trim2 = bezier_quadratic.trim(TValue::Parametric(0.75), TValue::Parametric(0.25)); 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(TValue::Parametric(0.25), TValue::Parametric(0.75)); - let trim4 = bezier_cubic.trim(TValue::Parametric(0.75), TValue::Parametric(0.25)).reverse(); + let trim4 = bezier_cubic.trim(TValue::Parametric(0.75), TValue::Parametric(0.25)); assert!(trim3.abs_diff_eq(&trim4, MAX_ABSOLUTE_DIFFERENCE)); } diff --git a/libraries/bezier-rs/src/bezier/compare.rs b/libraries/bezier-rs/src/compare.rs similarity index 77% rename from libraries/bezier-rs/src/bezier/compare.rs rename to libraries/bezier-rs/src/compare.rs index 51709c35..df94d4fb 100644 --- a/libraries/bezier-rs/src/bezier/compare.rs +++ b/libraries/bezier-rs/src/compare.rs @@ -1,5 +1,5 @@ /// Comparison functions used for tests in the bezier module -use super::{Bezier, CircleArc}; +use super::{Bezier, CircleArc, Subpath}; use crate::consts::MAX_ABSOLUTE_DIFFERENCE; use crate::utils::f64_compare; @@ -35,3 +35,9 @@ pub fn compare_arcs(arc1: CircleArc, arc2: CircleArc) -> bool { && f64_compare(arc1.start_angle, arc2.start_angle, MAX_ABSOLUTE_DIFFERENCE) && f64_compare(arc1.end_angle, arc2.end_angle, MAX_ABSOLUTE_DIFFERENCE) } + +/// Compare Subpath by verifying that their bezier segments match. +/// In this way, matching quadratic segments where the handles are on opposite manipulator groups will be considered equal. +pub fn compare_subpaths(subpath1: &Subpath, subpath2: &Subpath) -> bool { + subpath1.len() == subpath2.len() && subpath1.closed() == subpath2.closed() && subpath1.iter().eq(subpath2.iter()) +} diff --git a/libraries/bezier-rs/src/lib.rs b/libraries/bezier-rs/src/lib.rs index bc5f12a9..fa95fbb5 100644 --- a/libraries/bezier-rs/src/lib.rs +++ b/libraries/bezier-rs/src/lib.rs @@ -1,4 +1,6 @@ //! Bezier-rs: A Bezier Math Library for Rust +#[cfg(test)] +pub(crate) mod compare; mod bezier; mod consts; diff --git a/libraries/bezier-rs/src/subpath/core.rs b/libraries/bezier-rs/src/subpath/core.rs index 28b8d379..cdc80fad 100644 --- a/libraries/bezier-rs/src/subpath/core.rs +++ b/libraries/bezier-rs/src/subpath/core.rs @@ -62,7 +62,7 @@ impl Subpath { /// Returns an iterator of the [Bezier]s along the `Subpath`. pub fn iter(&self) -> SubpathIter { - SubpathIter { sub_path: self, index: 0 } + SubpathIter { subpath: self, index: 0 } } /// Appends to the `svg` mutable string with an SVG shape representation of the curve. diff --git a/libraries/bezier-rs/src/subpath/manipulators.rs b/libraries/bezier-rs/src/subpath/manipulators.rs index 329c7eed..2efbfecb 100644 --- a/libraries/bezier-rs/src/subpath/manipulators.rs +++ b/libraries/bezier-rs/src/subpath/manipulators.rs @@ -4,6 +4,11 @@ use crate::utils::f64_compare; use crate::{SubpathTValue, TValue}; impl Subpath { + /// Get whether the subpath is closed. + pub fn closed(&self) -> bool { + self.closed + } + /// 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: SubpathTValue) { diff --git a/libraries/bezier-rs/src/subpath/mod.rs b/libraries/bezier-rs/src/subpath/mod.rs index 02edd18c..87b49c00 100644 --- a/libraries/bezier-rs/src/subpath/mod.rs +++ b/libraries/bezier-rs/src/subpath/mod.rs @@ -8,6 +8,7 @@ pub use structs::*; use crate::Bezier; +use std::fmt::{Debug, Formatter, Result}; use std::ops::{Index, IndexMut}; /// Structure used to represent a path composed of [Bezier] curves. @@ -20,7 +21,7 @@ pub struct Subpath { /// Iteration structure for iterating across each curve of a `Subpath`, using an intermediate `Bezier` representation. pub struct SubpathIter<'a, ManipulatorGroupId: crate::Identifier> { index: usize, - sub_path: &'a Subpath, + subpath: &'a Subpath, } impl Index for Subpath { @@ -44,8 +45,8 @@ impl Iterator for SubpathIter<'_, Manipul // Returns the Bezier representation of each `Subpath` segment, defined between a pair of adjacent manipulator points. fn next(&mut self) -> Option { - let len = self.sub_path.len() - 1 - + match self.sub_path.closed { + let len = self.subpath.len() - 1 + + match self.subpath.closed { true => 1, false => 0, }; @@ -53,20 +54,15 @@ impl Iterator for SubpathIter<'_, Manipul return None; } let start_index = self.index; - let end_index = (self.index + 1) % self.sub_path.len(); + let end_index = (self.index + 1) % self.subpath.len(); self.index += 1; - let start = self.sub_path[start_index].anchor; - let end = self.sub_path[end_index].anchor; - let out_handle = self.sub_path[start_index].out_handle; - let in_handle = self.sub_path[end_index].in_handle; - - if let (Some(handle1), Some(handle2)) = (out_handle, in_handle) { - Some(Bezier::from_cubic_dvec2(start, handle1, handle2, end)) - } else if let Some(handle) = out_handle.or(in_handle) { - Some(Bezier::from_quadratic_dvec2(start, handle, end)) - } else { - Some(Bezier::from_linear_dvec2(start, end)) - } + Some(self.subpath[start_index].to_bezier(&self.subpath[end_index])) + } +} + +impl Debug for Subpath { + fn fmt(&self, f: &mut Formatter<'_>) -> Result { + f.debug_struct("Subpath").field("closed", &self.closed).field("manipulator_groups", &self.manipulator_groups).finish() } } diff --git a/libraries/bezier-rs/src/subpath/structs.rs b/libraries/bezier-rs/src/subpath/structs.rs index d5ef01b2..4a5a395c 100644 --- a/libraries/bezier-rs/src/subpath/structs.rs +++ b/libraries/bezier-rs/src/subpath/structs.rs @@ -31,15 +31,11 @@ pub struct ManipulatorGroup { 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) - } + f.debug_struct("ManipulatorGroup") + .field("anchor", &self.anchor) + .field("in_handle", &self.in_handle) + .field("out_handle", &self.out_handle) + .finish() } } diff --git a/libraries/bezier-rs/src/subpath/transform.rs b/libraries/bezier-rs/src/subpath/transform.rs index 00aa5f31..7e28f0e4 100644 --- a/libraries/bezier-rs/src/subpath/transform.rs +++ b/libraries/bezier-rs/src/subpath/transform.rs @@ -2,6 +2,17 @@ use super::*; use crate::utils::SubpathTValue; use crate::utils::TValue; +/// Helper function to ensure the index and t value pair is mapped within a maximum index value. +/// Allows for the point to be fetched without needing to handle an additional edge case. +/// - Ex. Via `subpath.iter().nth(index).evaluate(t);` +fn map_index_within_range(index: usize, t: f64, max_size: usize) -> (usize, f64) { + if max_size > 0 && index == max_size && t == 0. { + (index - 1, 1.) + } else { + (index, t) + } +} + /// 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`. @@ -78,13 +89,189 @@ impl Subpath { (Subpath::new(first_split, false), Some(Subpath::new(second_split, false))) } } + + /// Returns [ManipulatorGroup]s with a reversed winding order. + fn reverse_manipulator_groups(manipulator_groups: &[ManipulatorGroup]) -> Vec> { + manipulator_groups + .iter() + .rev() + .map(|group| ManipulatorGroup { + anchor: group.anchor, + in_handle: group.out_handle, + out_handle: group.in_handle, + id: ManipulatorGroupId::new(), + }) + .collect::>>() + } + + /// Returns a [Subpath] with a reversed winding order. + pub fn reverse(&self) -> Subpath { + Subpath { + manipulator_groups: Subpath::reverse_manipulator_groups(&self.manipulator_groups), + closed: self.closed, + } + } + + /// Returns an open [Subpath] that results from trimming the original Subpath between the points corresponding to `t1` and `t2`, maintaining the winding order of the original. + /// If the original Subpath is closed, the order of arguments does matter. + /// The resulting Subpath will wind from the given `t1` to `t2`. + /// That means, if the value of `t1` > `t2`, it will cross the break between endpoints from `t1` to `t = 1 = 0` to `t2`. + /// If a path winding in the reverse direction is desired, call `trim` on the `Subpath` returned from `Subpath::reverse`. + /// + pub fn trim(&self, t1: SubpathTValue, t2: SubpathTValue) -> Subpath { + // Return a clone of the Subpath if it is not long enough to be a valid Bezier + if self.manipulator_groups.is_empty() { + return Subpath { + manipulator_groups: vec![], + closed: self.closed, + }; + } + + let (mut t1_curve_index, mut t1_curve_t) = self.t_value_to_parametric(t1); + let (mut t2_curve_index, mut t2_curve_t) = self.t_value_to_parametric(t2); + + // The only case where t would be 1 is when the input parameter refers to the the very last point on the subpath. + // We want these index and t pairs to always represent that point as the next curve index with t == 0. + if t1_curve_t == 1. { + t1_curve_index += 1; + t1_curve_t = 0.; + } + if t2_curve_t == 1. { + t2_curve_index += 1; + t2_curve_t = 0.; + } + + // Check if the trimmed result is in the reverse direction + let are_arguments_reversed = t1_curve_index > t2_curve_index || (t1_curve_index == t2_curve_index && t1_curve_t > t2_curve_t); + if !self.closed && are_arguments_reversed { + (t1_curve_index, t2_curve_index) = (t2_curve_index, t1_curve_index); + (t1_curve_t, t2_curve_t) = (t2_curve_t, t1_curve_t); + } + + // Get a new list from the manipulator groups that will be trimmed at the ends to form the resulting subpath. + // The list will contain enough manipulator groups such that the later code simply needs to trim the first and last bezier segments + // and then update the values of the corresponding first and last manipulator groups accordingly. + let mut cloned_manipulator_groups = self.manipulator_groups.clone(); + let mut new_manipulator_groups = if self.closed && are_arguments_reversed { + // Need to rotate the cloned manipulator groups vector + // Remove the elements starting from t1_curve_index to become the new beginning of the list + let mut front = cloned_manipulator_groups.split_off(t1_curve_index); + // Truncate middle elements that are not needed + cloned_manipulator_groups.truncate(t2_curve_index + ((t2_curve_t != 0.) as usize) + 1); + // Reconnect the two ends in the new order + front.extend(cloned_manipulator_groups); + if t1_curve_index == t2_curve_index % self.len_segments() { + // If the start and end of the trim are in the same bezier segment, we want to add a duplicate of the first two manipulator groups. + // This is to make sure the the closed loop is correctly represented and because this segment needs to be trimmed on both ends of the resulting subpath. + front.push(front[0].clone()); + front.push(front[1].clone()); + } + if t1_curve_index == t2_curve_index % self.len_segments() + 1 { + // If the start and end of the trim are in adjacent bezier segments, we want to add a duplicate of the first manipulator group. + // This is to make sure the the closed loop is correctly represented. + front.push(front[0].clone()); + } + front + } else { + // Determine the subsection of the subpath's manipulator groups that are needed + if self.closed { + // Add a duplicate of the first manipulator group to ensure the final closing segment is considered + cloned_manipulator_groups.push(cloned_manipulator_groups[0].clone()); + } + + // Find the start and end of the new range and consider whether the indices are reversed + let range_start = t1_curve_index.min(t2_curve_index); + // Add 1 since the drain range is not inclusive + // Add 1 again if the corresponding t is not 0 because we want to include the next manipulator group which forms the bezier that this t value is on + let range_end = 1 + t2_curve_index + ((t2_curve_t != 0.) as usize); + + cloned_manipulator_groups + .drain(range_start..range_end.min(cloned_manipulator_groups.len())) + .collect::>>() + }; + + // Adjust curve indices to match the cloned list + if self.closed && are_arguments_reversed { + // If trimmed subpath required rotating the manipulator group, adjust the indices to match + t2_curve_index = (t2_curve_index + self.len_segments() - t1_curve_index) % self.len_segments(); + if t2_curve_index == 0 { + // If the case is where the start and end are in the same bezier, + // change the index to point to the duplicate of this bezier that was pushed to the vector + t2_curve_index += self.len_segments(); + } + t1_curve_index = 0; + } else { + let min_index = t1_curve_index.min(t2_curve_index); + t1_curve_index -= min_index; + t2_curve_index -= min_index; + } + + // Change the representation of the point corresponding to the end point of the subpath + // So that we do not need an additional edges case in the later code to handle this point + (t1_curve_index, t1_curve_t) = map_index_within_range(t1_curve_index, t1_curve_t, new_manipulator_groups.len() - 1); + (t2_curve_index, t2_curve_t) = map_index_within_range(t2_curve_index, t2_curve_t, new_manipulator_groups.len() - 1); + + if new_manipulator_groups.len() == 1 { + // This case will occur when `t1` and `t2` both represent one of the manipulator group anchors + // Add a duplicate manipulator group so that the returned Subpath is still a valid Bezier + let mut point = new_manipulator_groups[0].clone(); + point.in_handle = None; + point.out_handle = None; + return Subpath { + manipulator_groups: vec![point], + closed: false, + }; + } + + let len_new_manip_groups = new_manipulator_groups.len(); + + // Create Beziers from the first and last pairs of manipulator groups + // These will be trimmed to form the start and end of the new subpath + let curve1 = new_manipulator_groups[0].to_bezier(&new_manipulator_groups[1]); + let curve2 = new_manipulator_groups[len_new_manip_groups - 2].to_bezier(&new_manipulator_groups[len_new_manip_groups - 1]); + + // If the target curve_indices are the same, then the trim must be happening within one bezier + // This means curve1 == curve2 must be true, and we can simply call the Bezier trim. + if t1_curve_index == t2_curve_index { + return Subpath::from_bezier(curve1.trim(TValue::Parametric(t1_curve_t), TValue::Parametric(t2_curve_t))); + } + + // Split the bezier's with the according t value and keep the correct half + let [_, front_split] = curve1.split(TValue::Parametric(t1_curve_t)); + let [back_split, _] = curve2.split(TValue::Parametric(t2_curve_t)); + + // Update the first two manipulator groups to match the front_split + new_manipulator_groups[1].in_handle = front_split.handle_end(); + new_manipulator_groups[0] = ManipulatorGroup { + anchor: front_split.start(), + in_handle: None, + out_handle: front_split.handle_start(), + id: ManipulatorGroupId::new(), + }; + + // Update the last two manipulator groups to match the back_split + new_manipulator_groups[len_new_manip_groups - 2].out_handle = back_split.handle_start(); + new_manipulator_groups[len_new_manip_groups - 1] = ManipulatorGroup { + anchor: back_split.end(), + in_handle: back_split.handle_end(), + out_handle: None, + id: ManipulatorGroupId::new(), + }; + + Subpath { + manipulator_groups: new_manipulator_groups, + closed: false, + } + } } #[cfg(test)] mod tests { - use crate::utils::SubpathTValue; - - use super::*; + use super::{ManipulatorGroup, Subpath}; + use crate::compare::{compare_points, compare_subpaths, compare_vec_of_points}; + use crate::consts::MAX_ABSOLUTE_DIFFERENCE; + use crate::utils::{SubpathTValue, TValue}; + use crate::EmptyId; use glam::DVec2; fn set_up_open_subpath() -> Subpath { @@ -233,4 +420,303 @@ mod tests { assert_eq!(first.iter().last().unwrap(), subpath.iter().last().unwrap()); assert_eq!(first.iter().next().unwrap(), subpath.iter().next().unwrap()); } + + #[test] + fn reverse_an_open_subpath() { + let subpath = set_up_open_subpath(); + let temporary = subpath.reverse(); + let result = temporary.reverse(); + let end = result.len(); + + assert_eq!(temporary.manipulator_groups[0].anchor, result.manipulator_groups[end - 1].anchor); + assert_eq!(temporary.manipulator_groups[0].out_handle, result.manipulator_groups[end - 1].in_handle); + assert_eq!(subpath, result); + } + + #[test] + fn reverse_a_closed_subpath() { + let subpath = set_up_closed_subpath(); + let temporary = subpath.reverse(); + let result = temporary.reverse(); + let end = result.len(); + + assert_eq!(temporary.manipulator_groups[0].anchor, result.manipulator_groups[end - 1].anchor); + assert_eq!(temporary.manipulator_groups[0].in_handle, result.manipulator_groups[end - 1].out_handle); + assert_eq!(temporary.manipulator_groups[0].out_handle, result.manipulator_groups[end - 1].in_handle); + assert_eq!(subpath, result); + } + + #[test] + fn trim_an_open_subpath() { + let subpath = set_up_open_subpath(); + let location_front = subpath.evaluate(SubpathTValue::GlobalParametric(0.2)); + let location_back = subpath.evaluate(SubpathTValue::GlobalParametric(0.8)); + let [_, trim_front] = subpath.iter().next().unwrap().split(TValue::Parametric((0.2 * 3.) % 1.)); + let [trim_back, _] = subpath.iter().last().unwrap().split(TValue::Parametric((0.8 * 3.) % 1.)); + let result = subpath.trim(SubpathTValue::GlobalParametric(0.2), SubpathTValue::GlobalParametric(0.8)); + + assert_eq!(result.manipulator_groups[0].anchor, location_front); + assert_eq!(result.manipulator_groups[3].anchor, location_back); + assert_eq!(trim_front, result.iter().next().unwrap()); + assert_eq!(trim_back, result.iter().last().unwrap()); + } + + #[test] + fn trim_within_a_bezier() { + let subpath = set_up_open_subpath(); + let location_front = subpath.evaluate(SubpathTValue::GlobalParametric(0.1)); + let location_back = subpath.evaluate(SubpathTValue::GlobalParametric(0.2)); + let trimmed = subpath.iter().next().unwrap().trim(TValue::Parametric((0.1 * 3.) % 1.), TValue::Parametric((0.2 * 3.) % 1.)); + let result = subpath.trim(SubpathTValue::GlobalParametric(0.1), SubpathTValue::GlobalParametric(0.2)); + assert!(compare_points(result.manipulator_groups[0].anchor, location_front)); + assert!(compare_points(result.manipulator_groups[1].anchor, location_back)); + assert_eq!(trimmed, result.iter().next().unwrap()); + assert_eq!(result.len(), 2); + } + + #[test] + fn trim_first_segment_of_an_open_subpath() { + let subpath = set_up_closed_subpath(); + let location_front = subpath.evaluate(SubpathTValue::GlobalParametric(0.)); + let location_back = subpath.evaluate(SubpathTValue::GlobalParametric(0.25)); + let trimmed = subpath.iter().next().unwrap().trim(TValue::Parametric(0.), TValue::Parametric(1.)); + let result = subpath.trim(SubpathTValue::GlobalParametric(0.), SubpathTValue::GlobalParametric(0.25)); + + assert_eq!(result.manipulator_groups[0].anchor, location_front); + assert_eq!(result.manipulator_groups[1].anchor, location_back); + assert_eq!(trimmed, result.iter().next().unwrap()); + } + + #[test] + fn trim_second_segment_of_an_open_subpath() { + let subpath = set_up_closed_subpath(); + let location_front = subpath.evaluate(SubpathTValue::GlobalParametric(0.25)); + let location_back = subpath.evaluate(SubpathTValue::GlobalParametric(0.5)); + let trimmed = subpath.iter().nth(1).unwrap().trim(TValue::Parametric(0.), TValue::Parametric(1.)); + let result = subpath.trim(SubpathTValue::GlobalParametric(0.25), SubpathTValue::GlobalParametric(0.5)); + + assert_eq!(result.manipulator_groups[0].anchor, location_front); + assert_eq!(result.manipulator_groups[1].anchor, location_back); + assert_eq!(trimmed, result.iter().next().unwrap()); + } + + #[test] + fn trim_reverse_in_open_subpath() { + let subpath = set_up_open_subpath(); + let result1 = subpath.trim(SubpathTValue::GlobalParametric(0.8), SubpathTValue::GlobalParametric(0.2)); + let result2 = subpath.trim(SubpathTValue::GlobalParametric(0.2), SubpathTValue::GlobalParametric(0.8)); + + assert!(compare_subpaths(&result1, &result2)); + } + + #[test] + fn trim_reverse_within_a_bezier() { + let subpath = set_up_open_subpath(); + let location_front = subpath.evaluate(SubpathTValue::GlobalParametric(0.1)); + let location_back = subpath.evaluate(SubpathTValue::GlobalParametric(0.2)); + let trimmed = subpath.iter().next().unwrap().trim(TValue::Parametric((0.2 * 3.) % 1.), TValue::Parametric((0.1 * 3.) % 1.)); + let result = subpath.trim(SubpathTValue::GlobalParametric(0.2), SubpathTValue::GlobalParametric(0.1)); + + assert!(compare_points(result.manipulator_groups[0].anchor, location_front)); + assert!(compare_points(result.manipulator_groups[1].anchor, location_back)); + assert!(compare_vec_of_points( + trimmed.get_points().collect(), + result.iter().next().unwrap().get_points().collect(), + MAX_ABSOLUTE_DIFFERENCE + )); + assert_eq!(result.len(), 2); + } + + #[test] + fn trim_a_duplicate_subpath() { + let subpath = set_up_open_subpath(); + let location_front = subpath.evaluate(SubpathTValue::GlobalParametric(0.)); + let location_back = subpath.evaluate(SubpathTValue::GlobalParametric(1.)); + let result = subpath.trim(SubpathTValue::GlobalParametric(0.), SubpathTValue::GlobalParametric(1.)); + + // Assume that resulting subpath would no longer have the any meaningless handles + let mut expected_subpath = subpath.clone(); + expected_subpath[3].out_handle = None; + + assert_eq!(result.manipulator_groups[0].anchor, location_front); + assert!(compare_points(result.manipulator_groups[3].anchor, location_back)); + assert_eq!(expected_subpath, result); + } + + #[test] + fn trim_a_reversed_duplicate_subpath() { + let subpath = set_up_open_subpath(); + let location_front = subpath.evaluate(SubpathTValue::GlobalParametric(0.)); + let location_back = subpath.evaluate(SubpathTValue::GlobalParametric(1.)); + let result = subpath.trim(SubpathTValue::GlobalParametric(1.), SubpathTValue::GlobalParametric(0.)); + + assert_eq!(result.manipulator_groups[0].anchor, location_front); + assert_eq!(result.manipulator_groups[3].anchor, location_back); + assert!(compare_subpaths(&subpath, &result)); + } + + #[test] + fn trim_to_end_of_subpath() { + let subpath = set_up_open_subpath(); + let location_front = subpath.evaluate(SubpathTValue::GlobalParametric(0.8)); + let location_back = subpath.evaluate(SubpathTValue::GlobalParametric(1.)); + let trimmed = subpath.iter().last().unwrap().trim(TValue::Parametric((0.8 * 3.) % 1.), TValue::Parametric(1.)); + let result = subpath.trim(SubpathTValue::GlobalParametric(0.8), SubpathTValue::GlobalParametric(1.)); + + assert_eq!(result.manipulator_groups[0].anchor, location_front); + assert!(compare_points(result.manipulator_groups[1].anchor, location_back)); + assert_eq!(trimmed, result.iter().next().unwrap()); + } + + #[test] + fn trim_reversed_to_end_of_subpath() { + let subpath = set_up_open_subpath(); + let location_front = subpath.evaluate(SubpathTValue::GlobalParametric(0.)); + let location_back = subpath.evaluate(SubpathTValue::GlobalParametric(0.2)); + let trimmed = subpath.iter().next().unwrap().trim(TValue::Parametric((0.2 * 3.) % 1.), TValue::Parametric(0.)); + let result = subpath.trim(SubpathTValue::GlobalParametric(0.2), SubpathTValue::GlobalParametric(0.)); + + assert!(compare_points(result.manipulator_groups[0].anchor, location_front)); + assert!(compare_points(result.manipulator_groups[1].anchor, location_back)); + assert!(compare_vec_of_points( + trimmed.get_points().collect(), + result.iter().next().unwrap().get_points().collect(), + MAX_ABSOLUTE_DIFFERENCE + )); + } + + #[test] + fn trim_start_point() { + let subpath = set_up_open_subpath(); + let location = subpath.evaluate(SubpathTValue::GlobalParametric(0.)); + let result = subpath.trim(SubpathTValue::GlobalParametric(0.), SubpathTValue::GlobalParametric(0.)); + + assert!(compare_points(result.manipulator_groups[0].anchor, location)); + assert!(result.manipulator_groups[0].in_handle.is_none()); + assert!(result.manipulator_groups[0].out_handle.is_none()); + assert_eq!(result.len(), 1); + } + + #[test] + fn trim_middle_point() { + let subpath = set_up_closed_subpath(); + let location = subpath.evaluate(SubpathTValue::GlobalParametric(0.25)); + let result = subpath.trim(SubpathTValue::GlobalParametric(0.25), SubpathTValue::GlobalParametric(0.25)); + + assert!(compare_points(result.manipulator_groups[0].anchor, location)); + assert!(result.manipulator_groups[0].in_handle.is_none()); + assert!(result.manipulator_groups[0].out_handle.is_none()); + assert_eq!(result.len(), 1); + } + + #[test] + fn trim_a_closed_subpath() { + let subpath = set_up_closed_subpath(); + let location_front = subpath.evaluate(SubpathTValue::GlobalParametric(0.2)); + let location_back = subpath.evaluate(SubpathTValue::GlobalParametric(0.8)); + let [_, trim_front] = subpath.iter().next().unwrap().split(TValue::Parametric((0.2 * 4.) % 1.)); + let [trim_back, _] = subpath.iter().last().unwrap().split(TValue::Parametric((0.8 * 4.) % 1.)); + let result = subpath.trim(SubpathTValue::GlobalParametric(0.2), SubpathTValue::GlobalParametric(0.8)); + + assert_eq!(result.manipulator_groups[0].anchor, location_front); + assert_eq!(result.manipulator_groups[4].anchor, location_back); + assert_eq!(trim_front, result.iter().next().unwrap()); + assert_eq!(trim_back, result.iter().last().unwrap()); + } + + #[test] + fn trim_to_end_of_closed_subpath() { + let subpath = set_up_closed_subpath(); + let location_front = subpath.evaluate(SubpathTValue::GlobalParametric(0.8)); + let location_back = subpath.evaluate(SubpathTValue::GlobalParametric(1.)); + let trimmed = subpath.iter().last().unwrap().trim(TValue::Parametric((0.8 * 4.) % 1.), TValue::Parametric(1.)); + let result = subpath.trim(SubpathTValue::GlobalParametric(0.8), SubpathTValue::GlobalParametric(1.)); + + assert_eq!(result.manipulator_groups[0].anchor, location_front); + assert!(compare_points(result.manipulator_groups[1].anchor, location_back)); + assert_eq!(trimmed, result.iter().next().unwrap()); + } + + #[test] + fn trim_across_break_in_a_closed_subpath() { + let subpath = set_up_closed_subpath(); + let location_front = subpath.evaluate(SubpathTValue::GlobalParametric(0.8)); + let location_back = subpath.evaluate(SubpathTValue::GlobalParametric(0.2)); + let [_, trim_front] = subpath.iter().last().unwrap().split(TValue::Parametric((0.8 * 4.) % 1.)); + let [trim_back, _] = subpath.iter().next().unwrap().split(TValue::Parametric((0.2 * 4.) % 1.)); + let result = subpath.trim(SubpathTValue::GlobalParametric(0.8), SubpathTValue::GlobalParametric(0.2)); + + assert_eq!(result.manipulator_groups[0].anchor, location_front); + assert_eq!(result.manipulator_groups[2].anchor, location_back); + assert_eq!(trim_front, result.iter().next().unwrap()); + assert_eq!(trim_back, result.iter().last().unwrap()); + } + + #[test] + fn trim_across_break_in_a_closed_subpath_where_result_is_multiple_segments() { + let subpath = set_up_closed_subpath(); + let location_front = subpath.evaluate(SubpathTValue::GlobalParametric(0.6)); + let location_back = subpath.evaluate(SubpathTValue::GlobalParametric(0.4)); + let [_, trim_front] = subpath.iter().nth(2).unwrap().split(TValue::Parametric((0.6 * 4.) % 1.)); + let [trim_back, _] = subpath.iter().nth(1).unwrap().split(TValue::Parametric((0.4 * 4.) % 1.)); + let result = subpath.trim(SubpathTValue::GlobalParametric(0.6), SubpathTValue::GlobalParametric(0.4)); + + assert_eq!(result.manipulator_groups[0].anchor, location_front); + assert_eq!(result.manipulator_groups[4].anchor, location_back); + assert_eq!(trim_front, result.iter().next().unwrap()); + assert_eq!(trim_back, result.iter().last().unwrap()); + } + + #[test] + fn trim_across_break_in_a_closed_subpath_where_ends_are_in_same_segment() { + let subpath = set_up_closed_subpath(); + let location_front = subpath.evaluate(SubpathTValue::GlobalParametric(0.45)); + let location_back = subpath.evaluate(SubpathTValue::GlobalParametric(0.4)); + let [_, trim_front] = subpath.iter().nth(1).unwrap().split(TValue::Parametric((0.45 * 4.) % 1.)); + let [trim_back, _] = subpath.iter().nth(1).unwrap().split(TValue::Parametric((0.4 * 4.) % 1.)); + let result = subpath.trim(SubpathTValue::GlobalParametric(0.45), SubpathTValue::GlobalParametric(0.4)); + + assert_eq!(result.manipulator_groups[0].anchor, location_front); + assert_eq!(result.manipulator_groups[5].anchor, location_back); + assert_eq!(trim_front, result.iter().next().unwrap()); + assert_eq!(trim_back, result.iter().last().unwrap()); + } + + #[test] + fn trim_at_break_in_closed_subpath_where_end_is_0() { + let subpath = set_up_closed_subpath(); + let location_front = subpath.evaluate(SubpathTValue::GlobalParametric(0.8)); + let location_back = subpath.evaluate(SubpathTValue::GlobalParametric(0.)); + let trimmed = subpath.iter().last().unwrap().trim(TValue::Parametric((0.8 * 4.) % 1.), TValue::Parametric(1.)); + let result = subpath.trim(SubpathTValue::GlobalParametric(0.8), SubpathTValue::GlobalParametric(0.)); + + assert_eq!(result.manipulator_groups[0].anchor, location_front); + assert_eq!(result.manipulator_groups[1].anchor, location_back); + assert_eq!(trimmed, result.iter().next().unwrap()); + } + + #[test] + fn trim_at_break_in_closed_subpath_where_start_is_1() { + let subpath = set_up_closed_subpath(); + let location_front = subpath.evaluate(SubpathTValue::GlobalParametric(1.)); + let location_back = subpath.evaluate(SubpathTValue::GlobalParametric(0.2)); + let trimmed = subpath.iter().next().unwrap().trim(TValue::Parametric(0.), TValue::Parametric((0.2 * 4.) % 1.)); + let result = subpath.trim(SubpathTValue::GlobalParametric(1.), SubpathTValue::GlobalParametric(0.2)); + + assert_eq!(result.manipulator_groups[0].anchor, location_front); + assert_eq!(result.manipulator_groups[1].anchor, location_back); + assert_eq!(trimmed, result.iter().next().unwrap()); + } + + #[test] + fn trim_at_break_in_closed_subpath_from_1_to_0() { + let subpath = set_up_closed_subpath(); + let location = subpath.evaluate(SubpathTValue::GlobalParametric(0.)); + let result = subpath.trim(SubpathTValue::GlobalParametric(1.), SubpathTValue::GlobalParametric(0.)); + + assert_eq!(result.manipulator_groups[0].anchor, location); + assert!(result.manipulator_groups[0].in_handle.is_none()); + assert!(result.manipulator_groups[0].out_handle.is_none()); + assert_eq!(result.manipulator_groups.len(), 1); + } } 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 9384843a..32e703f6 100644 --- a/website/other/bezier-rs-demos/src/features/subpath-features.ts +++ b/website/other/bezier-rs-demos/src/features/subpath-features.ts @@ -105,6 +105,15 @@ const subpathFeatures = { sliderOptions: [tSliderOptions], chooseTVariant: true, }, + trim: { + name: "Trim", + callback: (subpath: WasmSubpathInstance, options: Record, _: undefined, tVariant: TVariant): string => subpath.trim(options.tVariant1, options.tVariant2, tVariant), + sliderOptions: [ + { ...tSliderOptions, default: 0.2, variable: "tVariant1" }, + { ...tSliderOptions, variable: "tVariant2" }, + ], + chooseTVariant: true, + }, }; export type SubpathFeatureKey = keyof typeof subpathFeatures; diff --git a/website/other/bezier-rs-demos/wasm/src/subpath.rs b/website/other/bezier-rs-demos/wasm/src/subpath.rs index b37ca554..9f751520 100644 --- a/website/other/bezier-rs-demos/wasm/src/subpath.rs +++ b/website/other/bezier-rs-demos/wasm/src/subpath.rs @@ -359,4 +359,21 @@ impl WasmSubpath { wrap_svg_tag(format!("{}{}{}", self.to_default_svg(), main_subpath_svg, other_subpath_svg)) } + + pub fn trim(&self, t1: f64, t2: f64, t_variant: String) -> String { + let t1 = parse_t_variant(&t_variant, t1); + let t2 = parse_t_variant(&t_variant, t2); + let trimmed_subpath = self.0.trim(t1, t2); + + let mut trimmed_subpath_svg = String::new(); + trimmed_subpath.to_svg( + &mut trimmed_subpath_svg, + CURVE_ATTRIBUTES.to_string().replace(BLACK, RED).replace("stroke-width=\"2\"", "stroke-width=\"8\"") + " opacity=\"0.5\"", + ANCHOR_ATTRIBUTES.to_string().replace(BLACK, RED), + HANDLE_ATTRIBUTES.to_string().replace(GRAY, RED), + HANDLE_LINE_ATTRIBUTES.to_string().replace(GRAY, RED), + ); + + wrap_svg_tag(format!("{}{}", self.to_default_svg(), trimmed_subpath_svg)) + } }