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 <keavon@keavon.com>
This commit is contained in:
Hannah Li 2023-02-26 18:41:11 -05:00 committed by Keavon Chambers
parent 4495488546
commit 8d3daeae78
13 changed files with 572 additions and 65 deletions

View file

@ -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() {

View file

@ -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::<Vec<DVec2>>())
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()
}
}

View file

@ -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() {

View file

@ -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`.
/// <iframe frameBorder="0" width="100%" height="450px" src="https://graphite.rs/bezier-rs-demos#bezier/trim/solo" title="Trim Demo"></iframe>
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));
}

View file

@ -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<ManipulatorGroupId: crate::Identifier>(subpath1: &Subpath<ManipulatorGroupId>, subpath2: &Subpath<ManipulatorGroupId>) -> bool {
subpath1.len() == subpath2.len() && subpath1.closed() == subpath2.closed() && subpath1.iter().eq(subpath2.iter())
}

View file

@ -1,4 +1,6 @@
//! Bezier-rs: A Bezier Math Library for Rust
#[cfg(test)]
pub(crate) mod compare;
mod bezier;
mod consts;

View file

@ -62,7 +62,7 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
/// Returns an iterator of the [Bezier]s along the `Subpath`.
pub fn iter(&self) -> SubpathIter<ManipulatorGroupId> {
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.

View file

@ -4,6 +4,11 @@ use crate::utils::f64_compare;
use crate::{SubpathTValue, TValue};
impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
/// 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) {

View file

@ -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<ManipulatorGroupId: crate::Identifier> {
/// 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<ManipulatorGroupId>,
subpath: &'a Subpath<ManipulatorGroupId>,
}
impl<ManipulatorGroupId: crate::Identifier> Index<usize> for Subpath<ManipulatorGroupId> {
@ -44,8 +45,8 @@ impl<ManipulatorGroupId: crate::Identifier> 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<Self::Item> {
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<ManipulatorGroupId: crate::Identifier> 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<ManipulatorGroupId: crate::Identifier> Debug for Subpath<ManipulatorGroupId> {
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
f.debug_struct("Subpath").field("closed", &self.closed).field("manipulator_groups", &self.manipulator_groups).finish()
}
}

View file

@ -31,15 +31,11 @@ pub struct ManipulatorGroup<ManipulatorGroupId: crate::Identifier> {
impl<ManipulatorGroupId: crate::Identifier> Debug for ManipulatorGroup<ManipulatorGroupId> {
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()
}
}

View file

@ -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<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
/// Returns either one or two Subpaths that result from splitting the original Subpath at the point corresponding to `t`.
@ -78,13 +89,189 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
(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<ManipulatorGroupId>]) -> Vec<ManipulatorGroup<ManipulatorGroupId>> {
manipulator_groups
.iter()
.rev()
.map(|group| ManipulatorGroup {
anchor: group.anchor,
in_handle: group.out_handle,
out_handle: group.in_handle,
id: ManipulatorGroupId::new(),
})
.collect::<Vec<ManipulatorGroup<ManipulatorGroupId>>>()
}
/// Returns a [Subpath] with a reversed winding order.
pub fn reverse(&self) -> Subpath<ManipulatorGroupId> {
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`.
/// <iframe frameBorder="0" width="100%" height="400px" src="https://graphite.rs/bezier-rs-demos#subpath/trim/solo" title="Trim Demo"></iframe>
pub fn trim(&self, t1: SubpathTValue, t2: SubpathTValue) -> Subpath<ManipulatorGroupId> {
// 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::<Vec<ManipulatorGroup<ManipulatorGroupId>>>()
};
// 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<EmptyId> {
@ -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);
}
}

View file

@ -105,6 +105,15 @@ const subpathFeatures = {
sliderOptions: [tSliderOptions],
chooseTVariant: true,
},
trim: {
name: "Trim",
callback: (subpath: WasmSubpathInstance, options: Record<string, number>, _: 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;

View file

@ -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))
}
}