Bezier-rs: Add parametric evaluate and line intersect to subpath (#852)

* add slider to subpath component + change evaluate to take an enum

Co-authored-by: Rob Nadal <RobNadal@users.noreply.github.com>

wip - add intersect to subpath, TODO fix bug

Co-authored-by: Rob Nadal <RobNadal@users.noreply.github.com>

add unit tests to subpath intersections

stress, testing

Co-authored-by: Hannah Li <hannahli2010@gmail.com>

* add parametric eval impl to subpath

* add line intersection to subpath

* Uncomment and #[ignore] disabled tests

* Reorder a few imports

* change subpath:eval slider to radio button

* fixed bug with solve_cubic, fixed unit tests, improved intersection accuracy

* fix failing test

Co-authored-by: Hannah Li <hannahli2010@gmail.com>
Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
Thomas Cheng
2022-12-11 00:41:02 -05:00
committed by Keavon Chambers
co-authored by Rob Nadal Hannah Li Keavon Chambers
parent 9a4af4f87a
commit 52cc770a1e
13 changed files with 713 additions and 54 deletions
+73 -15
View File
@@ -222,11 +222,36 @@ impl Bezier {
}
}
// TODO: Use an `impl Iterator` return type instead of a `Vec`
/// Returns a list of filtered `t` values that correspond to intersection points between the current bezier curve and the provided one
/// such that the difference between adjacent `t` values in sorted order is greater than some minimum seperation value. If the difference
/// between 2 adjacent `t` values is lesss than the minimum difference, the filtering takes the larger `t` value and discards the smaller `t` value.
/// The returned `t` values are with respect to the current bezier, not the provided parameter.
/// If the provided curve is linear, then zero intersection points will be returned along colinear segments.
/// - `error` - For intersections where the provided bezier is non-linear, `error` defines the threshold for bounding boxes to be considered an intersection point.
/// - `minimum_seperation` - The minimum difference between adjacent `t` values in sorted order
pub fn intersections(&self, other: &Bezier, error: Option<f64>, minimum_seperation: Option<f64>) -> Vec<f64> {
// TODO: Consider using the `intersections_between_vectors_of_curves` helper function here
// Otherwise, use bounding box to determine intersections
let mut intersection_t_values = self.unfiltered_intersections(other, error);
intersection_t_values.sort_by(|a, b| a.partial_cmp(b).unwrap());
// println!("<<<<< intersection_t_values :: {:?}", intersection_t_values);
intersection_t_values.iter().fold(Vec::new(), |mut accumulator, t| {
if !accumulator.is_empty() && (accumulator.last().unwrap() - t).abs() < minimum_seperation.unwrap_or(MIN_SEPERATION_VALUE) {
accumulator.pop();
}
accumulator.push(*t);
accumulator
})
}
// TODO: Use an `impl Iterator` return type instead of a `Vec`
/// Returns a list of `t` values that correspond to intersection points between the current bezier curve and the provided one. The returned `t` values are with respect to the current bezier, not the provided parameter.
/// If the provided curve is linear, then zero intersection points will be returned along colinear segments.
/// - `error` - For intersections where the provided bezier is non-linear, `error` defines the threshold for bounding boxes to be considered an intersection point.
pub fn intersections(&self, other: &Bezier, error: Option<f64>) -> Vec<f64> {
fn unfiltered_intersections(&self, other: &Bezier, error: Option<f64>) -> Vec<f64> {
let error = error.unwrap_or(0.5);
if other.handles == BezierHandles::Linear {
// Rotate the bezier and the line by the angle that the line makes with the x axis
@@ -295,7 +320,7 @@ impl Bezier {
let segment_pairs = subcurves1.iter().flat_map(move |(curve1, curve1_t_pair)| {
subcurves2
.iter()
.filter_map(move |(curve2, curve2_t_pair)| utils::do_rectangles_overlap(curve1.bounding_box(), curve2.bounding_box()).then(|| (curve1, curve1_t_pair, curve2, curve2_t_pair)))
.filter_map(move |(curve2, curve2_t_pair)| utils::do_rectangles_overlap(curve1.bounding_box(), curve2.bounding_box()).then_some((curve1, curve1_t_pair, curve2, curve2_t_pair)))
});
segment_pairs
.flat_map(|(curve1, curve1_t_pair, curve2, curve2_t_pair)| curve1.intersections_between_subcurves(curve1_t_pair.clone(), curve2, curve2_t_pair.clone(), error))
@@ -563,13 +588,13 @@ mod tests {
// Intersection at edge of curve
let bezier = Bezier::from_linear_dvec2(p1, p2);
let line1 = Bezier::from_linear_coordinates(20., 60., 70., 60.);
let intersections1 = bezier.intersections(&line1, None);
let intersections1 = bezier.intersections(&line1, None, None);
assert!(intersections1.len() == 1);
assert!(compare_points(bezier.evaluate(ComputeType::Parametric(intersections1[0])), DVec2::new(30., 60.)));
// Intersection in the middle of curve
let line2 = Bezier::from_linear_coordinates(150., 150., 30., 30.);
let intersections2 = bezier.intersections(&line2, None);
let intersections2 = bezier.intersections(&line2, None, None);
assert!(compare_points(bezier.evaluate(ComputeType::Parametric(intersections2[0])), DVec2::new(96., 96.)));
}
@@ -582,13 +607,13 @@ mod tests {
// Intersection at edge of curve
let bezier = Bezier::from_quadratic_dvec2(p1, p2, p3);
let line1 = Bezier::from_linear_coordinates(20., 50., 40., 50.);
let intersections1 = bezier.intersections(&line1, None);
let intersections1 = bezier.intersections(&line1, None, None);
assert!(intersections1.len() == 1);
assert!(compare_points(bezier.evaluate(ComputeType::Parametric(intersections1[0])), p1));
// Intersection in the middle of curve
let line2 = Bezier::from_linear_coordinates(150., 150., 30., 30.);
let intersections2 = bezier.intersections(&line2, None);
let intersections2 = bezier.intersections(&line2, None, None);
assert!(compare_points(bezier.evaluate(ComputeType::Parametric(intersections2[0])), DVec2::new(47.77355, 47.77354)));
}
@@ -602,30 +627,63 @@ mod tests {
let bezier = Bezier::from_cubic_dvec2(p1, p2, p3, p4);
// Intersection at edge of curve, Discriminant > 0
let line1 = Bezier::from_linear_coordinates(20., 30., 40., 30.);
let intersections1 = bezier.intersections(&line1, None);
let intersections1 = bezier.intersections(&line1, None, None);
assert!(intersections1.len() == 1);
assert!(compare_points(bezier.evaluate(ComputeType::Parametric(intersections1[0])), p1));
// Intersection at edge and in middle of curve, Discriminant < 0
let line2 = Bezier::from_linear_coordinates(150., 150., 30., 30.);
let intersections2 = bezier.intersections(&line2, None);
let intersections2 = bezier.intersections(&line2, None, None);
assert!(intersections2.len() == 2);
assert!(compare_points(bezier.evaluate(ComputeType::Parametric(intersections2[0])), p1));
assert!(compare_points(bezier.evaluate(ComputeType::Parametric(intersections2[1])), DVec2::new(85.84, 85.84)));
}
#[test]
fn test_intersect_curve_cubic_anchor_handle_overlap() {
// M31 94 C40 40 107 107 106 106
let p1 = DVec2::new(31., 94.);
let p2 = DVec2::new(40., 40.);
let p3 = DVec2::new(107., 107.);
let p4 = DVec2::new(106., 106.);
let bezier = Bezier::from_cubic_dvec2(p1, p2, p3, p4);
let line = Bezier::from_linear_coordinates(150., 150., 20., 20.);
let intersections = bezier.intersections(&line, None, None);
assert_eq!(intersections.len(), 1);
assert!(compare_points(bezier.evaluate(ComputeType::Parametric(intersections[0])), p4));
}
#[test]
fn test_intersect_curve_cubic_edge_case() {
// M34 107 C40 40 120 120 102 29
let p1 = DVec2::new(34., 107.);
let p2 = DVec2::new(40., 40.);
let p3 = DVec2::new(120., 120.);
let p4 = DVec2::new(102., 29.);
let bezier = Bezier::from_cubic_dvec2(p1, p2, p3, p4);
let line = Bezier::from_linear_coordinates(150., 150., 20., 20.);
let intersections = bezier.intersections(&line, None, None);
assert_eq!(intersections.len(), 1);
}
#[test]
fn test_intersect_curve() {
let bezier1 = Bezier::from_cubic_coordinates(30., 30., 60., 140., 150., 30., 160., 160.);
let bezier2 = Bezier::from_quadratic_coordinates(175., 140., 20., 20., 120., 20.);
let intersections = bezier1.intersections(&bezier2, None);
let intersections2 = bezier2.intersections(&bezier1, None);
assert!(compare_vec_of_points(
intersections.iter().map(|&t| bezier1.evaluate(ComputeType::Parametric(t))).collect(),
intersections2.iter().map(|&t| bezier2.evaluate(ComputeType::Parametric(t))).collect(),
2.
));
let intersections1 = bezier1.intersections(&bezier2, None, None);
let intersections2 = bezier2.intersections(&bezier1, None, None);
let intersections1_points: Vec<DVec2> = intersections1.iter().map(|&t| bezier1.evaluate(ComputeType::Parametric(t))).collect();
let intersections2_points: Vec<DVec2> = intersections2.iter().map(|&t| bezier2.evaluate(ComputeType::Parametric(t))).rev().collect();
assert!(compare_vec_of_points(intersections1_points, intersections2_points, 2.));
}
#[test]