mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2025-08-04 21:37:59 +00:00
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:
parent
a695584d36
commit
56060b8e5f
6 changed files with 321 additions and 5 deletions
|
@ -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 } => {
|
||||
|
|
|
@ -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.);
|
||||
|
|
|
@ -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::*;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue