Bezier-rs: Added curve outline functions (#789)

* Implement offset and reverse

Co-authored-by: Rob Nadal <robnadal44@gmail.com>

* Initial work on graduated outline

Co-authored-by: Rob Nadal <robnadal44@gmail.com>

* Handle linear case for graduated scale

* Added skewed outline, fixed graduated scale hourglass bug

* Removed test code

* Update comments

* Fix linting issue

* Improve comments

* Comment fixes

Co-authored-by: Hannah Li <hannahli2010@gmail.com>
Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
Rob Nadal 2022-11-06 21:25:00 -08:00 committed by Keavon Chambers
parent a695584d36
commit 56060b8e5f
6 changed files with 321 additions and 5 deletions

View file

@ -108,7 +108,7 @@ impl Bezier {
}
/// Return the string argument used to create a curve in an SVG `path`, excluding the start point.
pub(crate) fn svg_curve_argument(&self) -> String {
pub fn svg_curve_argument(&self) -> String {
let handle_args = match self.handles {
BezierHandles::Linear => SVG_ARG_LINEAR.to_string(),
BezierHandles::Quadratic { handle } => {

View file

@ -40,8 +40,18 @@ impl Bezier {
}
}
/// Returns a reversed version of the Bezier curve.
pub fn reverse(&self) -> Bezier {
match self.handles {
BezierHandles::Linear => Bezier::from_linear_dvec2(self.end, self.start),
BezierHandles::Quadratic { handle } => Bezier::from_quadratic_dvec2(self.end, handle, self.start),
BezierHandles::Cubic { handle_start, handle_end } => Bezier::from_cubic_dvec2(self.end, handle_end, handle_start, self.start),
}
}
/// Returns the Bezier curve representing the sub-curve starting at the point corresponding to `t1` and ending at the point corresponding to `t2`.
pub fn trim(&self, t1: f64, t2: f64) -> Bezier {
// 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(t1);
return match self.handles {
@ -63,7 +73,11 @@ impl Bezier {
// Case where we took the split from the beginning to `t1`
t2 / t1
};
bezier_starting_at_t1.split(adjusted_t2)[t2_split_side]
let result = bezier_starting_at_t1.split(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.
@ -243,11 +257,71 @@ impl Bezier {
})
}
/// Version of the `scale` function which scales the curve such that the start of the scaled curve is `start_distance` from the original curve, while the end of
/// of the scaled curve is `end_distance` from the original curve. The curve transitions from `start_distance` to `end_distance` gradually, proportional to the
/// distance along the equation (`t`-value) of the curve.
pub fn graduated_scale(&self, start_distance: f64, end_distance: f64) -> Bezier {
assert!(self.is_scalable(), "The curve provided to scale is not scalable. Reduce the curve first.");
let normal_start = self.normal(0.);
let normal_end = self.normal(1.);
// If normal unit vectors are equal, then the lines are parallel
if normal_start.abs_diff_eq(normal_end, MAX_ABSOLUTE_DIFFERENCE) {
let transformed_start = utils::scale_point_from_direction_vector(self.start, self.normal(0.), false, start_distance);
let transformed_end = utils::scale_point_from_direction_vector(self.end, self.normal(1.), false, end_distance);
return match self.handles {
BezierHandles::Linear => Bezier::from_linear_dvec2(transformed_start, transformed_end),
BezierHandles::Quadratic { handle } => {
let handle_closest_t = self.project(handle, ProjectionOptions::default());
let handle_scale_distance = (1. - handle_closest_t) * start_distance + handle_closest_t * end_distance;
let transformed_handle = utils::scale_point_from_direction_vector(handle, self.normal(handle_closest_t), false, handle_scale_distance);
Bezier::from_quadratic_dvec2(transformed_start, transformed_handle, transformed_end)
}
BezierHandles::Cubic { handle_start, handle_end } => {
let handle_start_closest_t = self.project(handle_start, ProjectionOptions::default());
let handle_start_scale_distance = (1. - handle_start_closest_t) * start_distance + handle_start_closest_t * end_distance;
let transformed_handle_start = utils::scale_point_from_direction_vector(handle_start, self.normal(handle_start_closest_t), false, handle_start_scale_distance);
let handle_end_closest_t = self.project(handle_start, ProjectionOptions::default());
let handle_end_scale_distance = (1. - handle_end_closest_t) * start_distance + handle_end_closest_t * end_distance;
let transformed_handle_end = utils::scale_point_from_direction_vector(handle_end, self.normal(handle_end_closest_t), false, handle_end_scale_distance);
Bezier::from_cubic_dvec2(transformed_start, transformed_handle_start, transformed_handle_end, transformed_end)
}
};
}
// Find the intersection point of the endpoint normals
let intersection = utils::line_intersection(self.start, normal_start, self.end, normal_end);
let should_flip_direction = (self.start - intersection).normalize().abs_diff_eq(normal_start, MAX_ABSOLUTE_DIFFERENCE);
let transformed_start = utils::scale_point_from_origin(self.start, intersection, should_flip_direction, start_distance);
let transformed_end = utils::scale_point_from_origin(self.end, intersection, should_flip_direction, end_distance);
match self.handles {
BezierHandles::Linear => Bezier::from_linear_dvec2(transformed_start, transformed_end),
BezierHandles::Quadratic { handle } => {
let handle_scale_distance = (start_distance + end_distance) / 2.;
let transformed_handle = utils::scale_point_from_origin(handle, intersection, should_flip_direction, handle_scale_distance);
Bezier::from_quadratic_dvec2(transformed_start, transformed_handle, transformed_end)
}
BezierHandles::Cubic { handle_start, handle_end } => {
let handle_start_scale_distance = (start_distance * 2. + end_distance) / 3.;
let transformed_handle_start = utils::scale_point_from_origin(handle_start, intersection, should_flip_direction, handle_start_scale_distance);
let handle_end_scale_distance = (start_distance + end_distance * 2.) / 3.;
let transformed_handle_end = utils::scale_point_from_origin(handle_end, intersection, should_flip_direction, handle_end_scale_distance);
Bezier::from_cubic_dvec2(transformed_start, transformed_handle_start, transformed_handle_end, transformed_end)
}
}
}
/// Offset will get all the reduceable subcurves, and for each subcurve, it will scale the subcurve a set distance away from the original curve.
/// Note that not all bezier curves are possible to offset, so this function first reduces the curve to scalable segments and then offsets those segments.
/// A proof for why this is true can be found in the [Curve offsetting section](https://pomax.github.io/bezierinfo/#offsetting) of Pomax's bezier curve primer.
/// Offset takes the following parameter:
/// - `distance` - The distance away from the curve that the new one will be offset to. Positive values will offset the curve in the same direction as the endpoint normals,
/// - `distance` - The offset's distance from the curve. Positive values will offset the curve in the same direction as the endpoint normals,
/// while negative values will offset in the opposite direction.
pub fn offset(&self, distance: f64) -> Vec<Bezier> {
let mut reduced = self.reduce(None);
@ -255,6 +329,64 @@ impl Bezier {
reduced
}
/// Version of the `offset` function which scales the offset such that the start of the offset is `start_distance` from the original curve, while the end of
/// of the offset is `end_distance` from the original curve. The curve transitions from `start_distance` to `end_distance` gradually, proportional to the
/// distance along the equation (`t`-value) of the curve. Similarily to the `offset` function, the returned result is an approximation.
pub fn graduated_offset(&self, start_distance: f64, end_distance: f64) -> Vec<Bezier> {
let reduced = self.reduce(None);
let mut next_start_distance = start_distance;
let distance_difference = end_distance - start_distance;
let total_length = self.length(None);
let mut result = vec![];
reduced.iter().for_each(|bezier| {
let current_length = bezier.length(None);
let next_end_distance = next_start_distance + (current_length / total_length) * distance_difference;
result.push(bezier.graduated_scale(next_start_distance, next_end_distance));
next_start_distance = next_end_distance;
});
result
}
/// Outline will return a vector of Beziers that creates an outline around the curve at the designated distance away from the curve.
/// It makes use of the `offset` function, thus restrictions applicable to `offset` are relevant to this function as well.
/// The 'caps', the linear segments at opposite ends of the outline, intersect the original curve at the midpoint of the cap.
///
/// Outline takes the following parameter:
/// - `distance` - The outline's distance from the curve.
pub fn outline(&self, distance: f64) -> Vec<Bezier> {
let first_segment = self.offset(distance);
let third_segment = self.reverse().offset(distance);
if first_segment.is_empty() || third_segment.is_empty() {
return vec![];
}
let second_segment = Bezier::from_linear_dvec2(first_segment.last().unwrap().end, third_segment.first().unwrap().start);
let fourth_segment = Bezier::from_linear_dvec2(third_segment.last().unwrap().end, first_segment.first().unwrap().start);
[first_segment, vec![second_segment], third_segment, vec![fourth_segment]].concat()
}
/// Version of the `outline` function which draws the outline at the specified distances away from the curve.
/// The outline begins `start_distance` away, and gradually move to being `end_distance` away.
pub fn graduated_outline(&self, start_distance: f64, end_distance: f64) -> Vec<Bezier> {
self.skewed_outline(start_distance, end_distance, end_distance, start_distance)
}
/// Version of the `graduated_outline` function that allows for the 4 corners of the outline to be different distances away from the curve.
pub fn skewed_outline(&self, distance1: f64, distance2: f64, distance3: f64, distance4: f64) -> Vec<Bezier> {
let first_segment = self.graduated_offset(distance1, distance2);
let third_segment = self.reverse().graduated_offset(distance3, distance4);
if first_segment.is_empty() || third_segment.is_empty() {
return vec![];
}
let second_segment = Bezier::from_linear_dvec2(first_segment.last().unwrap().end, third_segment.first().unwrap().start);
let fourth_segment = Bezier::from_linear_dvec2(third_segment.last().unwrap().end, first_segment.first().unwrap().start);
[first_segment, vec![second_segment], third_segment, vec![fourth_segment]].concat()
}
/// Approximate a bezier curve with circular arcs.
/// The algorithm can be customized using the [ArcsOptions] structure.
pub fn arcs(&self, arcs_options: ArcsOptions) -> Vec<CircleArc> {
@ -503,13 +635,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(0.25, 0.75);
let trim2 = bezier_quadratic.trim(0.75, 0.25);
let trim2 = bezier_quadratic.trim(0.75, 0.25).reverse();
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(0.25, 0.75);
let trim4 = bezier_cubic.trim(0.75, 0.25);
let trim4 = bezier_cubic.trim(0.75, 0.25).reverse();
assert!(trim3.abs_diff_eq(&trim4, MAX_ABSOLUTE_DIFFERENCE));
}
@ -599,6 +731,47 @@ mod tests {
assert!(compare_vector_of_beziers(&bezier2.offset(30.), expected_bezier_points2));
}
#[test]
fn test_outline() {
let p1 = DVec2::new(30., 50.);
let p2 = DVec2::new(140., 30.);
let line = Bezier::from_linear_dvec2(p1, p2);
let outline = line.outline(10.);
assert_eq!(outline.len(), 4);
// Assert the first length-wise piece of the outline is 10 units from the line
assert!(f64_compare(outline[0].evaluate(0.25).distance(line.evaluate(0.25)), 10., MAX_ABSOLUTE_DIFFERENCE)); // f64
// Assert the first cap touches the line end point at the halfway point
assert!(outline[1].evaluate(0.5).abs_diff_eq(line.end(), MAX_ABSOLUTE_DIFFERENCE));
// Assert the second length-wise piece of the outline is 10 units from the line
assert!(f64_compare(outline[2].evaluate(0.25).distance(line.evaluate(0.75)), 10., MAX_ABSOLUTE_DIFFERENCE)); // f64
// Assert the second cap touches the line start point at the halfway point
assert!(outline[3].evaluate(0.5).abs_diff_eq(line.start(), MAX_ABSOLUTE_DIFFERENCE));
}
#[test]
fn test_graduated_scale() {
let bezier = Bezier::from_linear_coordinates(30., 60., 140., 120.);
bezier.graduated_scale(10., 20.);
}
#[test]
fn test_graduated_scale_quadratic() {
let bezier = Bezier::from_quadratic_coordinates(30., 50., 82., 98., 160., 170.);
let scaled_bezier = bezier.graduated_scale(30., 30.);
dbg!(scaled_bezier);
// Assert the scaled bezier is 30 units from the line
assert!(f64_compare(scaled_bezier.evaluate(0.).distance(bezier.evaluate(0.)), 30., MAX_ABSOLUTE_DIFFERENCE));
assert!(f64_compare(scaled_bezier.evaluate(1.).distance(bezier.evaluate(1.)), 30., MAX_ABSOLUTE_DIFFERENCE));
assert!(f64_compare(scaled_bezier.evaluate(0.5).distance(bezier.evaluate(0.5)), 30., MAX_ABSOLUTE_DIFFERENCE));
}
#[test]
fn test_arcs_linear() {
let bezier = Bezier::from_linear_coordinates(30., 60., 140., 120.);

View file

@ -226,6 +226,17 @@ pub fn dvec2_approximately_in_range(point: DVec2, min: DVec2, max: DVec2, max_ab
(point.cmpge(min) & point.cmple(max)) | dvec2_compare(point, min, max_abs_diff) | dvec2_compare(point, max, max_abs_diff)
}
/// Calculate a new position for a point given its original position, a unit vector in the desired direction, and a distance to move it by.
pub fn scale_point_from_direction_vector(point: DVec2, direction_unit_vector: DVec2, should_flip_direction: bool, distance: f64) -> DVec2 {
let should_reverse_factor = if should_flip_direction { -1. } else { 1. };
point + distance * direction_unit_vector * should_reverse_factor
}
/// Scale a point by a given distance with respect to the provided origin.
pub fn scale_point_from_origin(point: DVec2, origin: DVec2, should_flip_direction: bool, distance: f64) -> DVec2 {
scale_point_from_direction_vector(point, (origin - point).normalize(), should_flip_direction, distance)
}
#[cfg(test)]
mod tests {
use super::*;