Skip to content
7 changes: 7 additions & 0 deletions libraries/bezier-rs/src/bezier/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,13 @@ impl Bezier {

self_points.len() == other_points.len() && self_points.into_iter().zip(other_points.into_iter()).all(|(a, b)| a.abs_diff_eq(b, max_abs_diff))
}

/// Returns true if the start, end and handles of the Bezier are all at the same location
pub fn is_point(&self) -> bool {
let start = self.start();

self.get_points().all(|point| point.abs_diff_eq(start, MAX_ABSOLUTE_DIFFERENCE))
}
}

#[cfg(test)]
Expand Down
13 changes: 9 additions & 4 deletions libraries/bezier-rs/src/bezier/solvers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,12 @@ impl Bezier {
/// <iframe frameBorder="0" width="100%" height="400px" src="https://graphite.rs/bezier-rs-demos#bezier/tangent/solo" title="Tangent Demo"></iframe>
pub fn tangent(&self, t: TValue) -> DVec2 {
let t = self.t_value_to_parametric(t);
self.non_normalized_tangent(t).normalize()
let tangent = self.non_normalized_tangent(t);
if tangent.length() > 0. {
tangent.normalize()
} else {
tangent
}
}

/// Returns a normalized unit vector representing the direction of the normal at the point `t` along the curve.
Expand All @@ -88,7 +93,7 @@ impl Bezier {

let numerator = d.x * dd.y - d.y * dd.x;
let denominator = (d.x.powf(2.) + d.y.powf(2.)).powf(1.5);
if denominator == 0. {
if denominator.abs() < MAX_ABSOLUTE_DIFFERENCE {
0.
} else {
numerator / denominator
Expand Down Expand Up @@ -369,9 +374,9 @@ impl Bezier {
}

// Create iterators that combine a subcurve with the `t` value pair that it was trimmed with
let combined_iterator1 = self1.into_iter().zip(self1_t_values.windows(2).map(|t_pair| Range { start: t_pair[0], end: t_pair[1] }));
let combined_iterator1 = self1.into_iter().zip(self1_t_values.iter().map(|t_pair| Range { start: t_pair[0], end: t_pair[1] }));
// Second one needs to be a list because Iterator does not implement copy
let combined_list2: Vec<(Bezier, Range<f64>)> = self2.into_iter().zip(self2_t_values.windows(2).map(|t_pair| Range { start: t_pair[0], end: t_pair[1] })).collect();
let combined_list2: Vec<(Bezier, Range<f64>)> = self2.into_iter().zip(self2_t_values.iter().map(|t_pair| Range { start: t_pair[0], end: t_pair[1] })).collect();

// For each curve, look for intersections with every curve that is at least 2 indices away
combined_iterator1
Expand Down
192 changes: 130 additions & 62 deletions libraries/bezier-rs/src/bezier/transform.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use super::*;

use crate::compare::compare_points;
use crate::utils::{f64_compare, TValue};
use crate::utils::{f64_compare, Cap, TValue};
use crate::{AppendType, ManipulatorGroup, Subpath};

use glam::DMat2;
Expand Down Expand Up @@ -158,16 +158,16 @@ impl Bezier {
// Verify the angle formed by the endpoint normals is sufficiently small, ensuring the on-curve point for `t = 0.5` occurs roughly in the center of the polygon.
let normal_0 = self.normal(TValue::Parametric(0.));
let normal_1 = self.normal(TValue::Parametric(1.));
let endpoint_normal_angle = (normal_0.x * normal_1.x + normal_0.y * normal_1.y).acos();
let endpoint_normal_angle = (normal_0.x * normal_1.x + normal_0.y * normal_1.y).min(1.).acos();
endpoint_normal_angle < SCALABLE_CURVE_MAX_ENDPOINT_NORMAL_ANGLE
}

/// Add the bezier endpoints if not already present, and combine and sort the dimensional extrema.
pub(crate) fn get_extrema_t_list(&self) -> Vec<f64> {
let mut extrema = self.local_extrema().into_iter().flatten().collect::<Vec<f64>>();
extrema.append(&mut vec![0., 1.]);
extrema.dedup();
extrema.sort_by(|ex1, ex2| ex1.partial_cmp(ex2).unwrap());
extrema.dedup();
extrema
}

Expand All @@ -176,10 +176,10 @@ impl Bezier {
/// The function takes the following parameter:
/// - `step_size` - Dictates the granularity at which the function searches for reducible subcurves. The default value is `0.01`.
/// A small granularity may increase the chance the function does not introduce gaps, but will increase computation time.
pub(crate) fn reduced_curves_and_t_values(&self, step_size: Option<f64>) -> (Vec<Bezier>, Vec<f64>) {
pub(crate) fn reduced_curves_and_t_values(&self, step_size: Option<f64>) -> (Vec<Bezier>, Vec<[f64; 2]>) {
// A linear segment is scalable, so return itself
if let BezierHandles::Linear = self.handles {
return (vec![*self], vec![0., 1.]);
return (vec![*self], vec![[0., 1.]]);
}

let step_size = step_size.unwrap_or(DEFAULT_REDUCE_STEP_SIZE);
Expand All @@ -192,7 +192,7 @@ impl Bezier {

// Split each subcurve such that each resulting segment is scalable.
let mut result_beziers: Vec<Bezier> = Vec::new();
let mut result_t_values: Vec<f64> = vec![extrema[0]];
let mut result_t_values: Vec<[f64; 2]> = vec![];

extrema.windows(2).for_each(|t_pair| {
let t_subcurve_start = t_pair[0];
Expand All @@ -201,29 +201,37 @@ impl Bezier {
// Perform no processing on the subcurve if it's already scalable.
if subcurve.is_scalable() {
result_beziers.push(subcurve);
result_t_values.push(t_subcurve_end);
result_t_values.push([t_subcurve_start, t_subcurve_end]);
return;
}

// Greedily iterate across the subcurve at intervals of size `step_size` to break up the curve into maximally large segments
let mut segment: Bezier;
let mut t1 = 0.;
let mut t2 = step_size;
let mut is_prev_valid = false;
while t2 <= 1. + step_size {
segment = subcurve.trim(TValue::Parametric(t1), TValue::Parametric(f64::min(t2, 1.)));
if !segment.is_scalable() {
t2 -= step_size;

// If the previous step does not exist, the start of the subcurve is irreducible.
// Otherwise, add the valid segment from the previous step to the result.
if f64::abs(t1 - t2) >= step_size {
if is_prev_valid {
segment = subcurve.trim(TValue::Parametric(t1), TValue::Parametric(t2));
result_beziers.push(segment);
result_t_values.push(t_subcurve_start + t2 * (t_subcurve_end - t_subcurve_start));
if segment.is_scalable() {
result_beziers.push(segment);
result_t_values.push([t_subcurve_start + t1 * (t_subcurve_end - t_subcurve_start), t_subcurve_start + t2 * (t_subcurve_end - t_subcurve_start)]);
} else {
t2 = t1 + step_size;
}
} else {
return;
t2 = t1 + step_size;
}
Comment thread
hannahli2010 marked this conversation as resolved.
t1 = t2;
is_prev_valid = false;
} else {
is_prev_valid = true;
}
t2 += step_size;
}
Expand All @@ -232,7 +240,7 @@ impl Bezier {
segment = subcurve.trim(TValue::Parametric(t1), TValue::Parametric(1.));
if segment.is_scalable() {
result_beziers.push(segment);
result_t_values.push(t_subcurve_end);
result_t_values.push([t_subcurve_start + t1 * (t_subcurve_end - t_subcurve_start), t_subcurve_end]);
}
}
});
Expand Down Expand Up @@ -349,14 +357,19 @@ impl Bezier {
/// while negative values will offset in the opposite direction.
/// <iframe frameBorder="0" width="100%" height="375px" src="https://graphite.rs/bezier-rs-demos#bezier/offset/solo" title="Offset Demo"></iframe>
pub fn offset<ManipulatorGroupId: crate::Identifier>(&self, distance: f64) -> Subpath<ManipulatorGroupId> {
if self.is_point() {
return Subpath::from_bezier(self);
}
let reduced = self.reduce(None);
let mut scaled = Subpath::new(vec![], false);
reduced.iter().enumerate().for_each(|(index, bezier)| {
let scaled_bezier = bezier.scale(distance);
if index > 0 && !compare_points(bezier.start(), reduced[index - 1].end()) {
scaled.append_bezier(&scaled_bezier, AppendType::SmoothJoin(MAX_ABSOLUTE_DIFFERENCE));
} else {
scaled.append_bezier(&scaled_bezier, AppendType::IgnoreStart);
if !bezier.is_point() {
if index > 0 && !compare_points(bezier.start(), reduced[index - 1].end()) {
scaled.append_bezier(&scaled_bezier, AppendType::SmoothJoin(MAX_ABSOLUTE_DIFFERENCE));
} else {
scaled.append_bezier(&scaled_bezier, AppendType::IgnoreStart);
}
}
});

Expand All @@ -376,19 +389,24 @@ impl Bezier {
let mut next_start_distance = start_distance;
let distance_difference = end_distance - start_distance;
let total_length = self.length(None);
if total_length < MAX_ABSOLUTE_DIFFERENCE {
return Subpath::new(vec![], false);
}

let mut result = Subpath::new(vec![], false);
reduced.iter().enumerate().for_each(|(index, bezier)| {
let current_length = bezier.length(None);
let next_end_distance = next_start_distance + (current_length / total_length) * distance_difference;
let scaled_bezier = bezier.graduated_scale(next_start_distance, next_end_distance);

if index > 0 && !compare_points(bezier.start(), reduced[index - 1].end()) {
result.append_bezier(&scaled_bezier, AppendType::SmoothJoin(MAX_ABSOLUTE_DIFFERENCE));
} else {
result.append_bezier(&scaled_bezier, AppendType::IgnoreStart);
if !bezier.is_point() {
let current_length = bezier.length(None);
let next_end_distance = next_start_distance + (current_length / total_length) * distance_difference;
let scaled_bezier = bezier.graduated_scale(next_start_distance, next_end_distance);

if index > 0 && !compare_points(bezier.start(), reduced[index - 1].end()) {
result.append_bezier(&scaled_bezier, AppendType::SmoothJoin(MAX_ABSOLUTE_DIFFERENCE));
} else {
result.append_bezier(&scaled_bezier, AppendType::IgnoreStart);
}
next_start_distance = next_end_distance;
}
next_start_distance = next_end_distance;
});

// If the curve is not linear, smooth the handles. All segments produced by bezier::scale will be cubic.
Expand All @@ -404,44 +422,48 @@ impl Bezier {
/// 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.
/// <iframe frameBorder="0" width="100%" height="375px" src="https://graphite.rs/bezier-rs-demos#bezier/outline/solo" title="Outline Demo"></iframe>
pub fn outline<ManipulatorGroupId: crate::Identifier>(&self, distance: f64) -> Subpath<ManipulatorGroupId> {
let first_segment = self.offset(distance);
let third_segment = self.reverse().offset(distance);
/// <iframe frameBorder="0" width="100%" height="400px" src="https://graphite.rs/bezier-rs-demos#bezier/outline/solo" title="Outline Demo"></iframe>
pub fn outline<ManipulatorGroupId: crate::Identifier>(&self, distance: f64, cap: Cap) -> Subpath<ManipulatorGroupId> {
let (pos_offset, neg_offset) = if self.is_point() {
(
Subpath::new(vec![ManipulatorGroup::new_anchor(self.start() + DVec2::NEG_Y * distance)], false),
Subpath::new(vec![ManipulatorGroup::new_anchor(self.start() + DVec2::Y * distance)], false),
)
} else {
(self.offset(distance), self.reverse().offset(distance))
};

if first_segment.is_empty() || third_segment.is_empty() {
if pos_offset.is_empty() || neg_offset.is_empty() {
return Subpath::new(vec![], false);
}

let mut result_manipulator_groups: Vec<ManipulatorGroup<ManipulatorGroupId>> = vec![];
result_manipulator_groups.extend_from_slice(first_segment.manipulator_groups());
// TODO: Handle other caps here
result_manipulator_groups.extend_from_slice(third_segment.manipulator_groups());
Subpath::new(result_manipulator_groups, true)
pos_offset.combine_outline(&neg_offset, cap)
}

/// 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.
/// <iframe frameBorder="0" width="100%" height="400px" src="https://graphite.rs/bezier-rs-demos#bezier/graduated-outline/solo" title="Graduated Outline Demo"></iframe>
pub fn graduated_outline<ManipulatorGroupId: crate::Identifier>(&self, start_distance: f64, end_distance: f64) -> Subpath<ManipulatorGroupId> {
self.skewed_outline(start_distance, end_distance, end_distance, start_distance)
/// <iframe frameBorder="0" width="100%" height="450px" src="https://graphite.rs/bezier-rs-demos#bezier/graduated-outline/solo" title="Graduated Outline Demo"></iframe>
pub fn graduated_outline<ManipulatorGroupId: crate::Identifier>(&self, start_distance: f64, end_distance: f64, cap: Cap) -> Subpath<ManipulatorGroupId> {
self.skewed_outline(start_distance, end_distance, end_distance, start_distance, cap)
}

/// Version of the `graduated_outline` function that allows for the 4 corners of the outline to be different distances away from the curve.
/// <iframe frameBorder="0" width="100%" height="475px" src="https://graphite.rs/bezier-rs-demos#bezier/skewed-outline/solo" title="Skewed Outline Demo"></iframe>
pub fn skewed_outline<ManipulatorGroupId: crate::Identifier>(&self, distance1: f64, distance2: f64, distance3: f64, distance4: f64) -> Subpath<ManipulatorGroupId> {
let first_segment = self.graduated_offset(distance1, distance2);
let third_segment = self.reverse().graduated_offset(distance3, distance4);
/// <iframe frameBorder="0" width="100%" height="550px" src="https://graphite.rs/bezier-rs-demos#bezier/skewed-outline/solo" title="Skewed Outline Demo"></iframe>
pub fn skewed_outline<ManipulatorGroupId: crate::Identifier>(&self, distance1: f64, distance2: f64, distance3: f64, distance4: f64, cap: Cap) -> Subpath<ManipulatorGroupId> {
let (pos_offset, neg_offset) = if self.is_point() {
(
Subpath::new(vec![ManipulatorGroup::new_anchor(self.start() + DVec2::NEG_Y * distance1)], false),
Subpath::new(vec![ManipulatorGroup::new_anchor(self.start() + DVec2::Y * distance1)], false),
)
} else {
(self.graduated_offset(distance1, distance2), self.reverse().graduated_offset(distance3, distance4))
};

if first_segment.is_empty() || third_segment.is_empty() {
if pos_offset.is_empty() || neg_offset.is_empty() {
return Subpath::new(vec![], false);
}

let mut result_manipulator_groups: Vec<ManipulatorGroup<ManipulatorGroupId>> = vec![];
result_manipulator_groups.extend_from_slice(first_segment.manipulator_groups());
// TODO: Handle other caps here
result_manipulator_groups.extend_from_slice(third_segment.manipulator_groups());
Subpath::new(result_manipulator_groups, true)
pos_offset.combine_outline(&neg_offset, cap)
}

/// Approximate a bezier curve with circular arcs.
Expand Down Expand Up @@ -596,8 +618,8 @@ impl Bezier {
#[cfg(test)]
mod tests {
use super::*;
use crate::compare::{compare_arcs, compare_points, compare_vec_of_points};
use crate::utils::TValue;
use crate::compare::{compare_arcs, compare_points};
use crate::utils::{Cap, TValue};
use crate::EmptyId;

#[test]
Expand Down Expand Up @@ -748,17 +770,8 @@ mod tests {
let p3 = DVec2::new(0., 0.);
let bezier = Bezier::from_quadratic_dvec2(p1, p2, p3);

let expected_bezier_points = vec![
vec![DVec2::new(0., 0.), DVec2::new(0.5, 0.5), DVec2::new(0.989, 0.989)],
vec![DVec2::new(0.989, 0.989), DVec2::new(2.705, 2.705), DVec2::new(4.2975, 4.2975)],
vec![DVec2::new(4.2975, 4.2975), DVec2::new(5.6625, 5.6625), DVec2::new(6.9375, 6.9375)],
];
let reduced_curves = bezier.reduce(None);
assert!(reduced_curves.iter().zip(expected_bezier_points.into_iter()).all(|(bezier, points)| compare_vec_of_points(
bezier.get_points().collect::<Vec<DVec2>>(),
points,
MAX_ABSOLUTE_DIFFERENCE
)));
assert!(reduced_curves.iter().all(|bezier| bezier.is_scalable()));

// Check that the reduce helper is correct
let (helper_curves, helper_t_values) = bezier.reduced_curves_and_t_values(None);
Expand All @@ -768,7 +781,7 @@ mod tests {
.all(|(bezier1, bezier2)| bezier1.abs_diff_eq(bezier2, MAX_ABSOLUTE_DIFFERENCE)));
assert!(reduced_curves
.iter()
.zip(helper_t_values.windows(2))
.zip(helper_t_values.iter())
.all(|(curve, t_pair)| curve.abs_diff_eq(&bezier.trim(TValue::Parametric(t_pair[0]), TValue::Parametric(t_pair[1])), MAX_ABSOLUTE_DIFFERENCE)))
}

Expand Down Expand Up @@ -853,12 +866,29 @@ mod tests {
}
}

#[test]
fn test_offset_curve_that_has_a_single_point_after_reduce() {
let p1 = DVec2::new(30., 30.);
let p2 = DVec2::new(150., 29.);
let p3 = DVec2::new(150., 30.);
let p4 = DVec2::new(160., 160.);

let bezier = Bezier::from_cubic_dvec2(p1, p2, p3, p4);

let reduce = bezier.reduce(None);
let offset = bezier.offset::<EmptyId>(15.);
assert!(reduce.last().is_some());
assert!(reduce.last().unwrap().is_point());
// Expect the single point bezier to be dropped in the offset
assert_eq!(reduce.len(), offset.len_segments() + 1);
}

#[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::<EmptyId>(10.);
let outline = line.outline::<EmptyId>(10., Cap::Butt);

assert_eq!(outline.len(), 4);

Expand All @@ -883,6 +913,44 @@ mod tests {
assert!(outline.iter().nth(3).unwrap().evaluate(TValue::Parametric(0.5)).abs_diff_eq(line.start(), MAX_ABSOLUTE_DIFFERENCE));
}

#[test]
fn test_outline_single_point_circle() {
let ellipse: Subpath<EmptyId> = Subpath::new_ellipse(DVec2::new(0., 0.), DVec2::new(50., 50.)).reverse();
let p = DVec2::new(25., 25.);

let line = Bezier::from_linear_dvec2(p, p);
let outline = line.outline::<EmptyId>(25., Cap::Round);
assert_eq!(outline, ellipse);

let cubic = Bezier::from_cubic_dvec2(p, p, p, p);
let outline_cubic = cubic.outline::<EmptyId>(25., Cap::Round);
assert_eq!(outline_cubic, ellipse);
}

#[test]
fn test_outline_single_point_square() {
let square: Subpath<EmptyId> = Subpath::from_anchors(
[
DVec2::new(25., 0.),
DVec2::new(0., 0.),
DVec2::new(0., 50.),
DVec2::new(25., 50.),
DVec2::new(50., 50.),
DVec2::new(50., 0.),
],
true,
);
let p = DVec2::new(25., 25.);

let line = Bezier::from_linear_dvec2(p, p);
let outline = line.outline::<EmptyId>(25., Cap::Square);
assert_eq!(outline, square);

let cubic = Bezier::from_cubic_dvec2(p, p, p, p);
let outline_cubic = cubic.outline::<EmptyId>(25., Cap::Square);
assert_eq!(outline_cubic, square);
}

#[test]
fn test_graduated_scale() {
let bezier = Bezier::from_linear_coordinates(30., 60., 140., 120.);
Expand Down
Loading