mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-27 14:18:11 +08:00
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:
co-authored by
Keavon Chambers
parent
ba50614af1
commit
8293f730f2
@@ -205,10 +205,9 @@ impl Bezier {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::utils::TValue;
|
|
||||||
|
|
||||||
use super::compare::compare_points;
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::compare::compare_points;
|
||||||
|
use crate::utils::TValue;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_quadratic_from_points() {
|
fn test_quadratic_from_points() {
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
#[cfg(test)]
|
|
||||||
pub(super) mod compare;
|
|
||||||
|
|
||||||
mod core;
|
mod core;
|
||||||
mod lookup;
|
mod lookup;
|
||||||
mod manipulators;
|
mod manipulators;
|
||||||
@@ -47,6 +44,13 @@ pub struct Bezier {
|
|||||||
|
|
||||||
impl Debug for Bezier {
|
impl Debug for Bezier {
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
|
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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -394,8 +394,8 @@ impl Bezier {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::compare::{compare_f64s, compare_points, compare_vec_of_points};
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::compare::{compare_f64s, compare_points, compare_vec_of_points};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_de_casteljau_points() {
|
fn test_de_casteljau_points() {
|
||||||
|
|||||||
@@ -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.
|
/// Returns the Bezier curve representing the sub-curve between the two provided points.
|
||||||
/// When `t1 < t2`, returns the reversed sub-curve starting at `t2` and ending at `t1`.
|
/// 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>
|
/// <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 {
|
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 t1 is equal to t2, return a bezier comprised entirely of the same point
|
||||||
if f64_compare(t1, t2, MAX_ABSOLUTE_DIFFERENCE) {
|
if f64_compare(t1, t2, MAX_ABSOLUTE_DIFFERENCE) {
|
||||||
let point = self.evaluate(TValue::Parametric(t1));
|
let point = self.evaluate(TValue::Parametric(t1));
|
||||||
@@ -64,25 +64,13 @@ impl Bezier {
|
|||||||
BezierHandles::Quadratic { handle: _ } => Bezier::from_quadratic_dvec2(point, point, point),
|
BezierHandles::Quadratic { handle: _ } => Bezier::from_quadratic_dvec2(point, point, point),
|
||||||
BezierHandles::Cubic { handle_start: _, handle_end: _ } => Bezier::from_cubic_dvec2(point, 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 bezier_ending_at_t2 = self.split(TValue::Parametric(t2))[0];
|
||||||
let t1_split_side = usize::from(t1 <= t2);
|
// Adjust the ratio `t1` to its corresponding value on the new curve that was split on `t2`
|
||||||
let t2_split_side = usize::from(t1 > t2);
|
let adjusted_t1 = t1 / t2;
|
||||||
let bezier_starting_at_t1 = self.split(TValue::Parametric(t1))[t1_split_side];
|
bezier_ending_at_t2.split(TValue::Parametric(adjusted_t1))[1]
|
||||||
// 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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a Bezier curve that results from applying the transformation function to each point in the Bezier.
|
/// 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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::utils::TValue;
|
|
||||||
|
|
||||||
use super::compare::{compare_arcs, compare_vector_of_beziers};
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::compare::{compare_arcs, compare_vector_of_beziers};
|
||||||
|
use crate::utils::TValue;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_split() {
|
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 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));
|
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.end(), cubic_bezier.evaluate(TValue::Parametric(0.75)));
|
||||||
assert_eq!(trimmed3.evaluate(TValue::Parametric(0.5)), cubic_bezier.evaluate(TValue::Parametric(0.5)));
|
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
|
// Test trimming quadratic curve when t2 > t1
|
||||||
let bezier_quadratic = Bezier::from_quadratic_coordinates(30., 50., 140., 30., 160., 170.);
|
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 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));
|
assert!(trim1.abs_diff_eq(&trim2, MAX_ABSOLUTE_DIFFERENCE));
|
||||||
|
|
||||||
// Test trimming cubic curve when t2 > t1
|
// Test trimming cubic curve when t2 > t1
|
||||||
let bezier_cubic = Bezier::from_cubic_coordinates(30., 30., 60., 140., 150., 30., 160., 160.);
|
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 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));
|
assert!(trim3.abs_diff_eq(&trim4, MAX_ABSOLUTE_DIFFERENCE));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/// Comparison functions used for tests in the bezier module
|
/// 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::consts::MAX_ABSOLUTE_DIFFERENCE;
|
||||||
use crate::utils::f64_compare;
|
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.start_angle, arc2.start_angle, MAX_ABSOLUTE_DIFFERENCE)
|
||||||
&& f64_compare(arc1.end_angle, arc2.end_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())
|
||||||
|
}
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
//! Bezier-rs: A Bezier Math Library for Rust
|
//! Bezier-rs: A Bezier Math Library for Rust
|
||||||
|
#[cfg(test)]
|
||||||
|
pub(crate) mod compare;
|
||||||
|
|
||||||
mod bezier;
|
mod bezier;
|
||||||
mod consts;
|
mod consts;
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
|
|||||||
|
|
||||||
/// Returns an iterator of the [Bezier]s along the `Subpath`.
|
/// Returns an iterator of the [Bezier]s along the `Subpath`.
|
||||||
pub fn iter(&self) -> SubpathIter<ManipulatorGroupId> {
|
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.
|
/// Appends to the `svg` mutable string with an SVG shape representation of the curve.
|
||||||
|
|||||||
@@ -4,6 +4,11 @@ use crate::utils::f64_compare;
|
|||||||
use crate::{SubpathTValue, TValue};
|
use crate::{SubpathTValue, TValue};
|
||||||
|
|
||||||
impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
|
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.
|
/// 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]`.
|
/// Expects `t` to be within the inclusive range `[0, 1]`.
|
||||||
pub fn insert(&mut self, t: SubpathTValue) {
|
pub fn insert(&mut self, t: SubpathTValue) {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ pub use structs::*;
|
|||||||
|
|
||||||
use crate::Bezier;
|
use crate::Bezier;
|
||||||
|
|
||||||
|
use std::fmt::{Debug, Formatter, Result};
|
||||||
use std::ops::{Index, IndexMut};
|
use std::ops::{Index, IndexMut};
|
||||||
|
|
||||||
/// Structure used to represent a path composed of [Bezier] curves.
|
/// 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.
|
/// Iteration structure for iterating across each curve of a `Subpath`, using an intermediate `Bezier` representation.
|
||||||
pub struct SubpathIter<'a, ManipulatorGroupId: crate::Identifier> {
|
pub struct SubpathIter<'a, ManipulatorGroupId: crate::Identifier> {
|
||||||
index: usize,
|
index: usize,
|
||||||
sub_path: &'a Subpath<ManipulatorGroupId>,
|
subpath: &'a Subpath<ManipulatorGroupId>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<ManipulatorGroupId: crate::Identifier> Index<usize> for 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.
|
// Returns the Bezier representation of each `Subpath` segment, defined between a pair of adjacent manipulator points.
|
||||||
fn next(&mut self) -> Option<Self::Item> {
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
let len = self.sub_path.len() - 1
|
let len = self.subpath.len() - 1
|
||||||
+ match self.sub_path.closed {
|
+ match self.subpath.closed {
|
||||||
true => 1,
|
true => 1,
|
||||||
false => 0,
|
false => 0,
|
||||||
};
|
};
|
||||||
@@ -53,20 +54,15 @@ impl<ManipulatorGroupId: crate::Identifier> Iterator for SubpathIter<'_, Manipul
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let start_index = self.index;
|
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;
|
self.index += 1;
|
||||||
|
|
||||||
let start = self.sub_path[start_index].anchor;
|
Some(self.subpath[start_index].to_bezier(&self.subpath[end_index]))
|
||||||
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) {
|
impl<ManipulatorGroupId: crate::Identifier> Debug for Subpath<ManipulatorGroupId> {
|
||||||
Some(Bezier::from_cubic_dvec2(start, handle1, handle2, end))
|
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
|
||||||
} else if let Some(handle) = out_handle.or(in_handle) {
|
f.debug_struct("Subpath").field("closed", &self.closed).field("manipulator_groups", &self.manipulator_groups).finish()
|
||||||
Some(Bezier::from_quadratic_dvec2(start, handle, end))
|
|
||||||
} else {
|
|
||||||
Some(Bezier::from_linear_dvec2(start, end))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,15 +31,11 @@ pub struct ManipulatorGroup<ManipulatorGroupId: crate::Identifier> {
|
|||||||
|
|
||||||
impl<ManipulatorGroupId: crate::Identifier> Debug for ManipulatorGroup<ManipulatorGroupId> {
|
impl<ManipulatorGroupId: crate::Identifier> Debug for ManipulatorGroup<ManipulatorGroupId> {
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
|
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
|
||||||
if self.in_handle.is_some() && self.out_handle.is_some() {
|
f.debug_struct("ManipulatorGroup")
|
||||||
write!(f, "anchor: {}, in: {}, out: {}", self.anchor, self.in_handle.unwrap(), self.out_handle.unwrap())
|
.field("anchor", &self.anchor)
|
||||||
} else if self.in_handle.is_some() {
|
.field("in_handle", &self.in_handle)
|
||||||
write!(f, "anchor: {}, in: {}, out: n/a", self.anchor, self.in_handle.unwrap())
|
.field("out_handle", &self.out_handle)
|
||||||
} else if self.out_handle.is_some() {
|
.finish()
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,17 @@ use super::*;
|
|||||||
use crate::utils::SubpathTValue;
|
use crate::utils::SubpathTValue;
|
||||||
use crate::utils::TValue;
|
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.
|
/// Functionality that transforms Subpaths, such as split, reduce, offset, etc.
|
||||||
impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
|
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`.
|
/// 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)))
|
(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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::utils::SubpathTValue;
|
use super::{ManipulatorGroup, Subpath};
|
||||||
|
use crate::compare::{compare_points, compare_subpaths, compare_vec_of_points};
|
||||||
use super::*;
|
use crate::consts::MAX_ABSOLUTE_DIFFERENCE;
|
||||||
|
use crate::utils::{SubpathTValue, TValue};
|
||||||
|
use crate::EmptyId;
|
||||||
use glam::DVec2;
|
use glam::DVec2;
|
||||||
|
|
||||||
fn set_up_open_subpath() -> Subpath<EmptyId> {
|
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().last().unwrap(), subpath.iter().last().unwrap());
|
||||||
assert_eq!(first.iter().next().unwrap(), subpath.iter().next().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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -105,6 +105,15 @@ const subpathFeatures = {
|
|||||||
sliderOptions: [tSliderOptions],
|
sliderOptions: [tSliderOptions],
|
||||||
chooseTVariant: true,
|
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;
|
export type SubpathFeatureKey = keyof typeof subpathFeatures;
|
||||||
|
|||||||
@@ -359,4 +359,21 @@ impl WasmSubpath {
|
|||||||
|
|
||||||
wrap_svg_tag(format!("{}{}{}", self.to_default_svg(), main_subpath_svg, other_subpath_svg))
|
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))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user