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 GitHub
co-authored by Keavon Chambers
parent ba50614af1
commit 8293f730f2
13 changed files with 571 additions and 64 deletions
-37
View File
@@ -1,37 +0,0 @@
/// Comparison functions used for tests in the bezier module
use super::{Bezier, CircleArc};
use crate::consts::MAX_ABSOLUTE_DIFFERENCE;
use crate::utils::f64_compare;
use glam::DVec2;
// Compare two f64s with some maximum absolute difference to account for floating point errors
pub fn compare_f64s(f1: f64, f2: f64) -> bool {
f64_compare(f1, f2, MAX_ABSOLUTE_DIFFERENCE)
}
/// Compare points by allowing some maximum absolute difference to account for floating point errors
pub fn compare_points(p1: DVec2, p2: DVec2) -> bool {
p1.abs_diff_eq(p2, MAX_ABSOLUTE_DIFFERENCE)
}
/// Compare vectors of points by allowing some maximum absolute difference to account for floating point errors
pub fn compare_vec_of_points(a: Vec<DVec2>, b: Vec<DVec2>, max_absolute_difference: f64) -> bool {
a.len() == b.len() && a.into_iter().zip(b.into_iter()).all(|(p1, p2)| p1.abs_diff_eq(p2, max_absolute_difference))
}
/// Compare vectors of beziers by allowing some maximum absolute difference between points to account for floating point errors
pub fn compare_vector_of_beziers(beziers: &[Bezier], expected_bezier_points: Vec<Vec<DVec2>>) -> bool {
beziers
.iter()
.zip(expected_bezier_points.iter())
.all(|(&a, b)| compare_vec_of_points(a.get_points().collect::<Vec<DVec2>>(), b.to_vec(), MAX_ABSOLUTE_DIFFERENCE))
}
/// Compare circle arcs by allowing some maximum absolute difference between values to account for floating point errors
pub fn compare_arcs(arc1: CircleArc, arc2: CircleArc) -> bool {
compare_points(arc1.center, arc2.center)
&& f64_compare(arc1.radius, arc1.radius, 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)
}
+2 -3
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() {
+8 -4
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()
}
}
+1 -1
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() {
+14 -27
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));
}