Bezier-rs: Updated Bezier function signatures to accept TValue (#967)

* Create helper for converting d to t values

* Add euclidean option for tangent and normal

* Modified bezier functions signatures to accept ComputeType

* Stylistic changes per review

* Added ComputeType documentation

* Renamed ComputeType to TValue

* Fixed comments

* Fixed failing unit tests

* Code review

* Fix comments in code review

* Renamed compute_type_to_parametric to t_value_to_parametric

---------

Co-authored-by: Linda Zheng <thelindazheng@gmail.com>
Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
Rob Nadal
2023-02-13 12:31:51 -05:00
committed by GitHub
co-authored by Linda Zheng Keavon Chambers
parent 1c2b8f67b2
commit 76be1f8515
25 changed files with 456 additions and 433 deletions
+59 -58
View File
@@ -1,15 +1,16 @@
use super::*;
use crate::utils::ComputeType;
use crate::utils::TValue;
use glam::DMat2;
use std::ops::Range;
/// Functionality that solve for various curve information such as derivative, tangent, intersect, etc.
impl Bezier {
/// Returns a list of lists of points representing the De Casteljau points for all iterations at the point corresponding to `t` using De Casteljau's algorithm.
/// Returns a list of lists of points representing the De Casteljau points for all iterations at the point `t` along the curve using De Casteljau's algorithm.
/// The `i`th element of the list represents the set of points in the `i`th iteration.
/// More information on the algorithm can be found in the [De Casteljau section](https://pomax.github.io/bezierinfo/#decasteljau) in Pomax's primer.
pub fn de_casteljau_points(&self, t: f64) -> Vec<Vec<DVec2>> {
pub fn de_casteljau_points(&self, t: TValue) -> Vec<Vec<DVec2>> {
let t = self.t_value_to_parametric(t);
let bezier_points = match self.handles {
BezierHandles::Linear => vec![self.start, self.end],
BezierHandles::Quadratic { handle } => vec![self.start, handle, self.end],
@@ -30,7 +31,7 @@ impl Bezier {
de_casteljau_points
}
/// Returns a Bezier representing the derivative of the original curve.
/// Returns a [Bezier] representing the derivative of the original curve.
/// - This function returns `None` for a linear segment.
pub fn derivative(&self) -> Option<Bezier> {
match self.handles {
@@ -49,27 +50,29 @@ impl Bezier {
}
}
/// Returns a normalized unit vector representing the tangent at the point designated by `t` on the curve.
pub fn tangent(&self, t: f64) -> DVec2 {
/// Returns a normalized unit vector representing the tangent at the point `t` along the curve.
pub fn tangent(&self, t: TValue) -> DVec2 {
let t = self.t_value_to_parametric(t);
match self.handles {
BezierHandles::Linear => self.end - self.start,
_ => self.derivative().unwrap().evaluate(ComputeType::Parametric(t)),
_ => self.derivative().unwrap().evaluate(TValue::Parametric(t)),
}
.normalize()
}
/// Returns a normalized unit vector representing the direction of the normal at the point designated by `t` on the curve.
pub fn normal(&self, t: f64) -> DVec2 {
/// Returns a normalized unit vector representing the direction of the normal at the point `t` along the curve.
pub fn normal(&self, t: TValue) -> DVec2 {
self.tangent(t).perp()
}
/// Returns the curvature, a scalar value for the derivative at the given `t`-value along the curve.
/// Returns the curvature, a scalar value for the derivative at the point `t` along the curve.
/// Curvature is 1 over the radius of a circle with an equivalent derivative.
pub fn curvature(&self, t: f64) -> f64 {
pub fn curvature(&self, t: TValue) -> f64 {
let t = self.t_value_to_parametric(t);
let (d, dd) = match &self.derivative() {
Some(first_derivative) => match first_derivative.derivative() {
Some(second_derivative) => (first_derivative.evaluate(ComputeType::Parametric(t)), second_derivative.evaluate(ComputeType::Parametric(t))),
None => (first_derivative.evaluate(ComputeType::Parametric(t)), first_derivative.end - first_derivative.start),
Some(second_derivative) => (first_derivative.evaluate(TValue::Parametric(t)), second_derivative.evaluate(TValue::Parametric(t))),
None => (first_derivative.evaluate(TValue::Parametric(t)), first_derivative.end - first_derivative.start),
},
None => (self.end - self.start, DVec2::new(0., 0.)),
};
@@ -129,7 +132,7 @@ impl Bezier {
let extrema = self.local_extrema();
for t_values in extrema {
for t in t_values {
let point = self.evaluate(ComputeType::Parametric(t));
let point = self.evaluate(TValue::Parametric(t));
// Update bounding box if new min/max is found.
endpoints_min = endpoints_min.min(point);
endpoints_max = endpoints_max.max(point);
@@ -178,7 +181,7 @@ impl Bezier {
}
}
/// Returns list of `t`-values representing the inflection points of the curve.
/// Returns list of parametric `t`-values representing the inflection points of the curve.
/// The list of `t`-values returned are filtered such that they fall within the range `[0, 1]`.
pub fn inflections(&self) -> Vec<f64> {
self.unrestricted_inflections().into_iter().filter(|&t| t > 0. && t < 1.).collect::<Vec<f64>>()
@@ -213,8 +216,8 @@ impl Bezier {
}
// Split curves in half and repeat with the combinations of the two halves of each curve
let [split_1_a, split_1_b] = self.split(0.5);
let [split_2_a, split_2_b] = other.split(0.5);
let [split_1_a, split_1_b] = self.split(TValue::Parametric(0.5));
let [split_2_a, split_2_b] = other.split(TValue::Parametric(0.5));
[
split_1_a.intersections_between_subcurves(self_start_t..self_mid_t, &split_2_a, other_start_t..other_mid_t, error),
@@ -229,7 +232,7 @@ 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
/// Returns a list of filtered parametric `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.
@@ -242,8 +245,6 @@ impl Bezier {
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();
@@ -334,7 +335,7 @@ impl Bezier {
}
// TODO: Use an `impl Iterator` return type instead of a `Vec`
/// Returns a list of `t` values that correspond to the self intersection points of the current bezier curve. For each intersection point, the returned `t` value is the smaller of the two that correspond to the point.
/// Returns a list of parametric `t` values that correspond to the self intersection points of the current bezier curve. For each intersection point, the returned `t` value is the smaller of the two that correspond to the point.
/// - `error` - For intersections with non-linear beziers, `error` defines the threshold for bounding boxes to be considered an intersection point.
pub fn self_intersections(&self, error: Option<f64>) -> Vec<[f64; 2]> {
if self.handles == BezierHandles::Linear || matches!(self.handles, BezierHandles::Quadratic { .. }) {
@@ -362,7 +363,7 @@ impl Bezier {
.collect()
}
/// Returns a list of `t` values that correspond to the intersection points between the curve and a rectangle defined by opposite corners.
/// Returns a list of parametric `t` values that correspond to the intersection points between the curve and a rectangle defined by opposite corners.
pub fn rectangle_intersections(&self, corner1: DVec2, corner2: DVec2) -> Vec<f64> {
[
Bezier::from_linear_coordinates(corner1.x, corner1.y, corner2.x, corner1.y),
@@ -384,7 +385,7 @@ mod tests {
#[test]
fn test_de_casteljau_points() {
let bezier = Bezier::from_cubic_coordinates(0., 0., 0., 100., 100., 100., 100., 0.);
let de_casteljau_points = bezier.de_casteljau_points(0.5);
let de_casteljau_points = bezier.de_casteljau_points(TValue::Parametric(0.5));
let expected_de_casteljau_points = vec![
vec![DVec2::new(0., 0.), DVec2::new(0., 100.), DVec2::new(100., 100.), DVec2::new(100., 0.)],
vec![DVec2::new(0., 50.), DVec2::new(50., 100.), DVec2::new(100., 50.)],
@@ -393,7 +394,7 @@ mod tests {
];
assert_eq!(&de_casteljau_points, &expected_de_casteljau_points);
assert_eq!(expected_de_casteljau_points[3][0], bezier.evaluate(ComputeType::Parametric(0.5)));
assert_eq!(expected_de_casteljau_points[3][0], bezier.evaluate(TValue::Parametric(0.5)));
}
#[test]
@@ -433,16 +434,16 @@ mod tests {
let linear = Bezier::from_linear_dvec2(p1, p2);
let unit_slope = DVec2::new(30., 20.).normalize();
assert_eq!(linear.tangent(0.), unit_slope);
assert_eq!(linear.tangent(1.), unit_slope);
assert_eq!(linear.tangent(TValue::Parametric(0.)), unit_slope);
assert_eq!(linear.tangent(TValue::Parametric(1.)), unit_slope);
let quadratic = Bezier::from_quadratic_dvec2(p1, p2, p3);
assert_eq!(quadratic.tangent(0.), DVec2::new(60., 40.).normalize());
assert_eq!(quadratic.tangent(1.), DVec2::new(40., 60.).normalize());
assert_eq!(quadratic.tangent(TValue::Parametric(0.)), DVec2::new(60., 40.).normalize());
assert_eq!(quadratic.tangent(TValue::Parametric(1.)), DVec2::new(40., 60.).normalize());
let cubic = Bezier::from_cubic_dvec2(p1, p2, p3, p4);
assert_eq!(cubic.tangent(0.), DVec2::new(90., 60.).normalize());
assert_eq!(cubic.tangent(1.), DVec2::new(30., 120.).normalize());
assert_eq!(cubic.tangent(TValue::Parametric(0.)), DVec2::new(90., 60.).normalize());
assert_eq!(cubic.tangent(TValue::Parametric(1.)), DVec2::new(30., 120.).normalize());
}
#[test]
@@ -455,16 +456,16 @@ mod tests {
let linear = Bezier::from_linear_dvec2(p1, p2);
let unit_slope = DVec2::new(-20., 30.).normalize();
assert_eq!(linear.normal(0.), unit_slope);
assert_eq!(linear.normal(1.), unit_slope);
assert_eq!(linear.normal(TValue::Parametric(0.)), unit_slope);
assert_eq!(linear.normal(TValue::Parametric(1.)), unit_slope);
let quadratic = Bezier::from_quadratic_dvec2(p1, p2, p3);
assert_eq!(quadratic.normal(0.), DVec2::new(-40., 60.).normalize());
assert_eq!(quadratic.normal(1.), DVec2::new(-60., 40.).normalize());
assert_eq!(quadratic.normal(TValue::Parametric(0.)), DVec2::new(-40., 60.).normalize());
assert_eq!(quadratic.normal(TValue::Parametric(1.)), DVec2::new(-60., 40.).normalize());
let cubic = Bezier::from_cubic_dvec2(p1, p2, p3, p4);
assert_eq!(cubic.normal(0.), DVec2::new(-60., 90.).normalize());
assert_eq!(cubic.normal(1.), DVec2::new(-120., 30.).normalize());
assert_eq!(cubic.normal(TValue::Parametric(0.)), DVec2::new(-60., 90.).normalize());
assert_eq!(cubic.normal(TValue::Parametric(1.)), DVec2::new(-120., 30.).normalize());
}
#[test]
@@ -475,24 +476,24 @@ mod tests {
let p4 = DVec2::new(50., 10.);
let linear = Bezier::from_linear_dvec2(p1, p2);
assert_eq!(linear.curvature(0.), 0.);
assert_eq!(linear.curvature(0.5), 0.);
assert_eq!(linear.curvature(1.), 0.);
assert_eq!(linear.curvature(TValue::Parametric(0.)), 0.);
assert_eq!(linear.curvature(TValue::Parametric(0.5)), 0.);
assert_eq!(linear.curvature(TValue::Parametric(1.)), 0.);
let quadratic = Bezier::from_quadratic_dvec2(p1, p2, p3);
assert!(compare_f64s(quadratic.curvature(0.), 0.0125));
assert!(compare_f64s(quadratic.curvature(0.5), 0.035355));
assert!(compare_f64s(quadratic.curvature(1.), 0.0125));
assert!(compare_f64s(quadratic.curvature(TValue::Parametric(0.)), 0.0125));
assert!(compare_f64s(quadratic.curvature(TValue::Parametric(0.5)), 0.035355));
assert!(compare_f64s(quadratic.curvature(TValue::Parametric(1.)), 0.0125));
let cubic = Bezier::from_cubic_dvec2(p1, p2, p3, p4);
assert!(compare_f64s(cubic.curvature(0.), 0.016667));
assert!(compare_f64s(cubic.curvature(0.5), 0.));
assert!(compare_f64s(cubic.curvature(1.), 0.));
assert!(compare_f64s(cubic.curvature(TValue::Parametric(0.)), 0.016667));
assert!(compare_f64s(cubic.curvature(TValue::Parametric(0.5)), 0.));
assert!(compare_f64s(cubic.curvature(TValue::Parametric(1.)), 0.));
// The curvature at an inflection point is zero
let inflection_curve = Bezier::from_cubic_coordinates(30., 30., 30., 150., 150., 30., 150., 150.);
let inflections = inflection_curve.inflections();
assert_eq!(inflection_curve.curvature(inflections[0]), 0.);
assert_eq!(inflection_curve.curvature(TValue::Parametric(inflections[0])), 0.);
}
#[test]
@@ -609,12 +610,12 @@ mod tests {
let line1 = Bezier::from_linear_coordinates(20., 60., 70., 60.);
let intersections1 = bezier.intersections(&line1, None, None);
assert!(intersections1.len() == 1);
assert!(compare_points(bezier.evaluate(ComputeType::Parametric(intersections1[0])), DVec2::new(30., 60.)));
assert!(compare_points(bezier.evaluate(TValue::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, None);
assert!(compare_points(bezier.evaluate(ComputeType::Parametric(intersections2[0])), DVec2::new(96., 96.)));
assert!(compare_points(bezier.evaluate(TValue::Parametric(intersections2[0])), DVec2::new(96., 96.)));
}
#[test]
@@ -628,12 +629,12 @@ mod tests {
let line1 = Bezier::from_linear_coordinates(20., 50., 40., 50.);
let intersections1 = bezier.intersections(&line1, None, None);
assert!(intersections1.len() == 1);
assert!(compare_points(bezier.evaluate(ComputeType::Parametric(intersections1[0])), p1));
assert!(compare_points(bezier.evaluate(TValue::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, None);
assert!(compare_points(bezier.evaluate(ComputeType::Parametric(intersections2[0])), DVec2::new(47.77355, 47.77354)));
assert!(compare_points(bezier.evaluate(TValue::Parametric(intersections2[0])), DVec2::new(47.77355, 47.77354)));
}
#[test]
@@ -648,14 +649,14 @@ mod tests {
let line1 = Bezier::from_linear_coordinates(20., 30., 40., 30.);
let intersections1 = bezier.intersections(&line1, None, None);
assert!(intersections1.len() == 1);
assert!(compare_points(bezier.evaluate(ComputeType::Parametric(intersections1[0])), p1));
assert!(compare_points(bezier.evaluate(TValue::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, 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)));
assert!(compare_points(bezier.evaluate(TValue::Parametric(intersections2[0])), p1));
assert!(compare_points(bezier.evaluate(TValue::Parametric(intersections2[1])), DVec2::new(85.84, 85.84)));
}
#[test]
@@ -672,7 +673,7 @@ mod tests {
let intersections = bezier.intersections(&line, None, None);
assert_eq!(intersections.len(), 1);
assert!(compare_points(bezier.evaluate(ComputeType::Parametric(intersections[0])), p4));
assert!(compare_points(bezier.evaluate(TValue::Parametric(intersections[0])), p4));
}
#[test]
@@ -699,8 +700,8 @@ mod tests {
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();
let intersections1_points: Vec<DVec2> = intersections1.iter().map(|&t| bezier1.evaluate(TValue::Parametric(t))).collect();
let intersections2_points: Vec<DVec2> = intersections2.iter().map(|&t| bezier2.evaluate(TValue::Parametric(t))).rev().collect();
assert!(compare_vec_of_points(intersections1_points, intersections2_points, 2.));
}
@@ -710,8 +711,8 @@ mod tests {
let bezier = Bezier::from_cubic_coordinates(160., 180., 170., 10., 30., 90., 180., 140.);
let intersections = bezier.self_intersections(Some(0.5));
assert!(compare_vec_of_points(
intersections.iter().map(|&t| bezier.evaluate(ComputeType::Parametric(t[0]))).collect(),
intersections.iter().map(|&t| bezier.evaluate(ComputeType::Parametric(t[1]))).collect(),
intersections.iter().map(|&t| bezier.evaluate(TValue::Parametric(t[0]))).collect(),
intersections.iter().map(|&t| bezier.evaluate(TValue::Parametric(t[1]))).collect(),
2.
));
assert!(Bezier::from_linear_coordinates(160., 180., 170., 10.).self_intersections(None).is_empty());