diff --git a/libraries/bezier-rs/src/bezier/core.rs b/libraries/bezier-rs/src/bezier/core.rs
index bc391259..7f3886c2 100644
--- a/libraries/bezier-rs/src/bezier/core.rs
+++ b/libraries/bezier-rs/src/bezier/core.rs
@@ -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)]
diff --git a/libraries/bezier-rs/src/bezier/solvers.rs b/libraries/bezier-rs/src/bezier/solvers.rs
index b0b771f9..18be83ec 100644
--- a/libraries/bezier-rs/src/bezier/solvers.rs
+++ b/libraries/bezier-rs/src/bezier/solvers.rs
@@ -64,7 +64,12 @@ impl Bezier {
///
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.
@@ -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
@@ -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)> = 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)> = 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
diff --git a/libraries/bezier-rs/src/bezier/transform.rs b/libraries/bezier-rs/src/bezier/transform.rs
index 5e255587..4df23d0d 100644
--- a/libraries/bezier-rs/src/bezier/transform.rs
+++ b/libraries/bezier-rs/src/bezier/transform.rs
@@ -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;
@@ -158,7 +158,7 @@ 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
}
@@ -166,8 +166,8 @@ impl Bezier {
pub(crate) fn get_extrema_t_list(&self) -> Vec {
let mut extrema = self.local_extrema().into_iter().flatten().collect::>();
extrema.append(&mut vec![0., 1.]);
- extrema.dedup();
extrema.sort_by(|ex1, ex2| ex1.partial_cmp(ex2).unwrap());
+ extrema.dedup();
extrema
}
@@ -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) -> (Vec, Vec) {
+ pub(crate) fn reduced_curves_and_t_values(&self, step_size: Option) -> (Vec, 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);
@@ -192,7 +192,7 @@ impl Bezier {
// Split each subcurve such that each resulting segment is scalable.
let mut result_beziers: Vec = Vec::new();
- let mut result_t_values: Vec = 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];
@@ -201,7 +201,7 @@ 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;
}
@@ -209,6 +209,7 @@ impl Bezier {
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() {
@@ -216,14 +217,21 @@ impl Bezier {
// 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;
}
t1 = t2;
+ is_prev_valid = false;
+ } else {
+ is_prev_valid = true;
}
t2 += step_size;
}
@@ -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]);
}
}
});
@@ -349,14 +357,19 @@ impl Bezier {
/// while negative values will offset in the opposite direction.
///
pub fn offset(&self, distance: f64) -> Subpath {
+ 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);
+ }
}
});
@@ -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 !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);
+ 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.
@@ -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.
- ///
- pub fn outline(&self, distance: f64) -> Subpath {
- let first_segment = self.offset(distance);
- let third_segment = self.reverse().offset(distance);
+ ///
+ pub fn outline(&self, distance: f64, cap: Cap) -> Subpath {
+ 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> = 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.
- ///
- pub fn graduated_outline(&self, start_distance: f64, end_distance: f64) -> Subpath {
- self.skewed_outline(start_distance, end_distance, end_distance, start_distance)
+ ///
+ pub fn graduated_outline(&self, start_distance: f64, end_distance: f64, cap: Cap) -> Subpath {
+ 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.
- ///
- pub fn skewed_outline(&self, distance1: f64, distance2: f64, distance3: f64, distance4: f64) -> Subpath {
- let first_segment = self.graduated_offset(distance1, distance2);
- let third_segment = self.reverse().graduated_offset(distance3, distance4);
+ ///
+ pub fn skewed_outline(&self, distance1: f64, distance2: f64, distance3: f64, distance4: f64, cap: Cap) -> Subpath {
+ 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> = 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.
@@ -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]
@@ -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::>(),
- 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);
@@ -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)))
}
@@ -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::(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::(10.);
+ let outline = line.outline::(10., Cap::Butt);
assert_eq!(outline.len(), 4);
@@ -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 = 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::(25., Cap::Round);
+ assert_eq!(outline, ellipse);
+
+ let cubic = Bezier::from_cubic_dvec2(p, p, p, p);
+ let outline_cubic = cubic.outline::(25., Cap::Round);
+ assert_eq!(outline_cubic, ellipse);
+ }
+
+ #[test]
+ fn test_outline_single_point_square() {
+ let square: Subpath = 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::(25., Cap::Square);
+ assert_eq!(outline, square);
+
+ let cubic = Bezier::from_cubic_dvec2(p, p, p, p);
+ let outline_cubic = cubic.outline::(25., Cap::Square);
+ assert_eq!(outline_cubic, square);
+ }
+
#[test]
fn test_graduated_scale() {
let bezier = Bezier::from_linear_coordinates(30., 60., 140., 120.);
diff --git a/libraries/bezier-rs/src/lib.rs b/libraries/bezier-rs/src/lib.rs
index 61a7fd7b..cc74ff03 100644
--- a/libraries/bezier-rs/src/lib.rs
+++ b/libraries/bezier-rs/src/lib.rs
@@ -8,4 +8,4 @@ mod utils;
pub use bezier::*;
pub use subpath::*;
-pub use utils::{Joint, SubpathTValue, TValue};
+pub use utils::{Cap, Join, SubpathTValue, TValue};
diff --git a/libraries/bezier-rs/src/subpath/core.rs b/libraries/bezier-rs/src/subpath/core.rs
index de6768ae..1b7b67f7 100644
--- a/libraries/bezier-rs/src/subpath/core.rs
+++ b/libraries/bezier-rs/src/subpath/core.rs
@@ -88,7 +88,7 @@ impl Subpath {
/// Returns the number of segments contained within the `Subpath`.
pub fn len_segments(&self) -> usize {
let mut number_of_curves = self.len();
- if !self.closed {
+ if !self.closed && number_of_curves > 0 {
number_of_curves -= 1
}
number_of_curves
@@ -112,6 +112,17 @@ impl Subpath {
&self.manipulator_groups
}
+ /// Returns if the Subpath is equivalent to a single point.
+ pub fn is_point(&self) -> bool {
+ if self.is_empty() {
+ return false;
+ }
+ let point = self.manipulator_groups[0].anchor;
+ self.manipulator_groups
+ .iter()
+ .all(|manipulator_group| manipulator_group.anchor.abs_diff_eq(point, MAX_ABSOLUTE_DIFFERENCE))
+ }
+
/// Appends to the `svg` mutable string with an SVG shape representation of the curve.
pub fn curve_to_svg(&self, svg: &mut String, attributes: String) {
let curve_start_argument = format!("{SVG_ARG_MOVE}{} {}", self[0].anchor.x, self[0].anchor.y);
diff --git a/libraries/bezier-rs/src/subpath/solvers.rs b/libraries/bezier-rs/src/subpath/solvers.rs
index 7b93fd0b..166a5e1f 100644
--- a/libraries/bezier-rs/src/subpath/solvers.rs
+++ b/libraries/bezier-rs/src/subpath/solvers.rs
@@ -1,9 +1,10 @@
use super::*;
use crate::consts::MAX_ABSOLUTE_DIFFERENCE;
-use crate::utils::SubpathTValue;
+use crate::utils::{compute_circular_subpath_details, line_intersection, SubpathTValue};
use crate::TValue;
-use glam::DVec2;
+use glam::{DMat2, DVec2};
+use std::f64::consts::PI;
impl Subpath {
/// Calculate the point on the subpath based on the parametric `t`-value provided.
@@ -22,7 +23,7 @@ impl Subpath {
/// - `error`: an optional f64 value to provide an error bound
/// - `minimum_separation`: the minimum difference two adjacent `t`-values must have when comparing adjacent `t`-values in sorted order.
/// If the comparison condition is not satisfied, the function takes the larger `t`-value of the two.
- ///
+ ///
pub fn intersections(&self, other: &Bezier, error: Option, minimum_separation: Option) -> Vec<(usize, f64)> {
self.iter()
.enumerate()
@@ -34,27 +35,20 @@ impl Subpath {
/// This function expects the following:
/// - other: a [Bezier] curve to check intersections against
/// - error: an optional f64 value to provide an error bound
- ///
+ ///
pub fn subpath_intersections(&self, other: &Subpath, error: Option, minimum_separation: Option) -> Vec<(usize, f64)> {
let mut intersection_t_values: Vec<(usize, f64)> = other.iter().flat_map(|bezier| self.intersections(&bezier, error, minimum_separation)).collect();
intersection_t_values.sort_by(|a, b| a.partial_cmp(b).unwrap());
intersection_t_values
}
- /// Returns a normalized unit vector representing the tangent on the subpath based on the parametric `t`-value provided.
- ///
- pub fn tangent(&self, t: SubpathTValue) -> DVec2 {
- let (segment_index, t) = self.t_value_to_parametric(t);
- self.get_segment(segment_index).unwrap().tangent(TValue::Parametric(t))
- }
-
/// Returns a list of `t` values that correspond to the self intersection points of the subpath. 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.
/// - `minimum_separation`: the minimum difference two adjacent `t`-values must have when comparing adjacent `t`-values in sorted order.
/// If the comparison condition is not satisfied, the function takes the larger `t`-value of the two
///
/// **NOTE**: if an intersection were to occur within an `error` distance away from an anchor point, the algorithm will filter that intersection out.
- ///
+ ///
pub fn self_intersections(&self, error: Option, minimum_separation: Option) -> Vec<(usize, f64)> {
let mut intersections_vec = Vec::new();
let err = error.unwrap_or(MAX_ABSOLUTE_DIFFERENCE);
@@ -74,6 +68,13 @@ impl Subpath {
intersections_vec
}
+ /// Returns a normalized unit vector representing the tangent on the subpath based on the parametric `t`-value provided.
+ ///
+ pub fn tangent(&self, t: SubpathTValue) -> DVec2 {
+ let (segment_index, t) = self.t_value_to_parametric(t);
+ self.get_segment(segment_index).unwrap().tangent(TValue::Parametric(t))
+ }
+
/// Returns a normalized unit vector representing the direction of the normal on the subpath based on the parametric `t`-value provided.
///
pub fn normal(&self, t: SubpathTValue) -> DVec2 {
@@ -83,7 +84,7 @@ impl Subpath {
/// Returns two lists of `t`-values representing the local extrema of the `x` and `y` parametric subpaths respectively.
/// The list of `t`-values returned are filtered such that they fall within the range `[0, 1]`.
- ///
+ ///
pub fn local_extrema(&self) -> [Vec; 2] {
let number_of_curves = self.len_segments() as f64;
@@ -98,7 +99,7 @@ impl Subpath {
}
/// Return the min and max corners that represent the bounding box of the subpath.
- ///
+ ///
pub fn bounding_box(&self) -> Option<[DVec2; 2]> {
self.iter().map(|bezier| bezier.bounding_box()).reduce(|bbox1, bbox2| [bbox1[0].min(bbox2[0]), bbox1[1].max(bbox2[1])])
}
@@ -112,7 +113,7 @@ impl Subpath {
/// Returns list of `t`-values representing the inflection points of the subpath.
/// The list of `t`-values returned are filtered such that they fall within the range `[0, 1]`.
- ///
+ ///
pub fn inflections(&self) -> Vec {
let number_of_curves = self.len_segments() as f64;
let inflection_t_values: Vec = self
@@ -135,6 +136,92 @@ impl Subpath {
pub fn contains_point(&self, target_point: DVec2) -> bool {
self.iter().map(|bezier| bezier.winding(target_point)).sum::() != 0
}
+
+ /// Returns the manipulator point that is needed for a miter join if it is possible.
+ pub(crate) fn miter_line_join(&self, other: &Subpath) -> Option> {
+ let in_segment = self.get_segment(self.len_segments() - 1).unwrap();
+ let out_segment = other.get_segment(0).unwrap();
+ let in_tangent = in_segment.tangent(TValue::Parametric(1.));
+ let out_tangent = out_segment.tangent(TValue::Parametric(0.));
+
+ let normalized_in_tangent = in_tangent.normalize();
+ let normalized_out_tangent = out_tangent.normalize();
+
+ // The tangents must not be parallel for the miter join
+ if !normalized_in_tangent.abs_diff_eq(normalized_out_tangent, MAX_ABSOLUTE_DIFFERENCE) && !normalized_in_tangent.abs_diff_eq(-normalized_out_tangent, MAX_ABSOLUTE_DIFFERENCE) {
+ let intersection = line_intersection(in_segment.end(), in_tangent, out_segment.start(), out_tangent);
+
+ // Draw the miter join if the intersection occurs in the correct direction with respect to the path
+ if (intersection - in_segment.end()).normalize().abs_diff_eq(in_tangent, MAX_ABSOLUTE_DIFFERENCE)
+ && (out_segment.start() - intersection).normalize().abs_diff_eq(out_tangent, MAX_ABSOLUTE_DIFFERENCE)
+ {
+ return Some(ManipulatorGroup {
+ anchor: intersection,
+ in_handle: None,
+ out_handle: None,
+ id: ManipulatorGroupId::new(),
+ });
+ }
+ }
+ // If we can't draw the miter join, default to a bevel join
+ None
+ }
+
+ /// Returns the necessary information to create a round join with the provided center.
+ /// The returned items correspond to:
+ /// - The `out_handle` for the last manipulator group of `self`
+ /// - The new manipulator group to be added
+ /// - The `in_handle` for the first manipulator group of `other`
+ pub(crate) fn round_line_join(&self, other: &Subpath, center: DVec2) -> (DVec2, ManipulatorGroup, DVec2) {
+ let left = self.manipulator_groups[self.len() - 1].anchor;
+ let right = other.manipulator_groups[0].anchor;
+
+ let center_to_right = right - center;
+ let center_to_left = left - center;
+
+ let in_segment = self.get_segment(self.len_segments() - 1).unwrap();
+ let in_tangent = in_segment.tangent(TValue::Parametric(1.));
+
+ let mut angle = center_to_right.angle_between(center_to_left) / 2.;
+ let mut arc_point = center + DMat2::from_angle(angle).mul_vec2(center_to_right);
+
+ if (arc_point - left).angle_between(in_tangent).abs() > PI / 2. {
+ angle = angle - PI * (if angle < 0. { -1. } else { 1. });
+ arc_point = center + DMat2::from_angle(angle).mul_vec2(center_to_right);
+ }
+
+ compute_circular_subpath_details(left, arc_point, right, center, Some(angle))
+ }
+
+ /// Returns the necessary information to create a round cap between the end of `self` and the beginning of `other`.
+ /// The returned items correspond to:
+ /// - The `out_handle` for the last manipulator group of `self`
+ /// - The new manipulator group to be added
+ /// - The `in_handle` for the first manipulator group of `other`
+ pub(crate) fn round_cap(&self, other: &Subpath) -> (DVec2, ManipulatorGroup, DVec2) {
+ let left = self.manipulator_groups[self.len() - 1].anchor;
+ let right = other.manipulator_groups[0].anchor;
+
+ let center = (right + left) / 2.;
+ let center_to_right = right - center;
+
+ let arc_point = center + center_to_right.perp();
+
+ compute_circular_subpath_details(left, arc_point, right, center, None)
+ }
+
+ /// Returns the two manipulator groups that create a sqaure cap between the end of `self` and the beginning of `other`.
+ pub(crate) fn square_cap(&self, other: &Subpath) -> [ManipulatorGroup; 2] {
+ let left = self.manipulator_groups[self.len() - 1].anchor;
+ let right = other.manipulator_groups[0].anchor;
+
+ let center = (right + left) / 2.;
+ let center_to_right = right - center;
+
+ let translation = center_to_right.perp();
+
+ [ManipulatorGroup::new_anchor(left + translation), ManipulatorGroup::new_anchor(right + translation)]
+ }
}
#[cfg(test)]
@@ -517,4 +604,86 @@ mod tests {
}
// TODO: add more intersection tests
+
+ #[test]
+ fn round_join_counter_clockwise_rotation() {
+ // Test case where the round join is drawn in the counter clockwise direction between two consecutive offsets
+ let subpath = Subpath::new(
+ vec![
+ ManipulatorGroup {
+ anchor: DVec2::new(20., 20.),
+ out_handle: Some(DVec2::new(10., 90.)),
+ in_handle: None,
+ id: EmptyId,
+ },
+ ManipulatorGroup {
+ anchor: DVec2::new(114., 159.),
+ out_handle: None,
+ in_handle: Some(DVec2::new(60., 40.)),
+ id: EmptyId,
+ },
+ ManipulatorGroup {
+ anchor: DVec2::new(148., 155.),
+ out_handle: None,
+ in_handle: None,
+ id: EmptyId,
+ },
+ ],
+ false,
+ );
+
+ let offset = subpath.offset(10., utils::Join::Round);
+ let offset_len = offset.len();
+
+ let manipulator_groups = offset.manipulator_groups();
+ let round_start = manipulator_groups[offset_len - 4].anchor;
+ let round_point = manipulator_groups[offset_len - 3].anchor;
+ let round_end = manipulator_groups[offset_len - 2].anchor;
+
+ let middle = (round_start + round_end) / 2.;
+
+ assert!((round_point - middle).angle_between(round_start - middle) > 0.);
+ assert!((round_end - middle).angle_between(round_point - middle) > 0.);
+ }
+
+ #[test]
+ fn round_join_clockwise_rotation() {
+ // Test case where the round join is drawn in the clockwise direction between two consecutive offsets
+ let subpath = Subpath::new(
+ vec![
+ ManipulatorGroup {
+ anchor: DVec2::new(20., 20.),
+ out_handle: Some(DVec2::new(10., 90.)),
+ in_handle: None,
+ id: EmptyId,
+ },
+ ManipulatorGroup {
+ anchor: DVec2::new(150., 40.),
+ out_handle: None,
+ in_handle: Some(DVec2::new(60., 40.)),
+ id: EmptyId,
+ },
+ ManipulatorGroup {
+ anchor: DVec2::new(78., 36.),
+ out_handle: None,
+ in_handle: None,
+ id: EmptyId,
+ },
+ ],
+ false,
+ );
+
+ let offset = subpath.offset(-15., utils::Join::Round);
+ let offset_len = offset.len();
+
+ let manipulator_groups = offset.manipulator_groups();
+ let round_start = manipulator_groups[offset_len - 4].anchor;
+ let round_point = manipulator_groups[offset_len - 3].anchor;
+ let round_end = manipulator_groups[offset_len - 2].anchor;
+
+ let middle = (round_start + round_end) / 2.;
+
+ assert!((round_point - middle).angle_between(round_start - middle) < 0.);
+ assert!((round_end - middle).angle_between(round_point - middle) < 0.);
+ }
}
diff --git a/libraries/bezier-rs/src/subpath/transform.rs b/libraries/bezier-rs/src/subpath/transform.rs
index 2233b756..0087bce0 100644
--- a/libraries/bezier-rs/src/subpath/transform.rs
+++ b/libraries/bezier-rs/src/subpath/transform.rs
@@ -2,9 +2,9 @@ use std::vec;
use super::*;
use crate::consts::MAX_ABSOLUTE_DIFFERENCE;
-use crate::utils::{Joint, SubpathTValue, TValue};
+use crate::utils::{Cap, Join, SubpathTValue, TValue};
-use glam::DAffine2;
+use glam::{DAffine2, DVec2};
/// Helper function to ensure the index and t value pair is mapped within a maximum index value.
/// Allows for the point to be fetched without needing to handle an additional edge case.
@@ -109,9 +109,14 @@ impl Subpath {
}
/// Returns a [Subpath] with a reversed winding order.
+ /// Note that a reversed closed subpath will start on the same manipulator group and simply wind the other direction
pub fn reverse(&self) -> Subpath {
+ let mut reversed = Subpath::reverse_manipulator_groups(self.manipulator_groups());
+ if self.closed {
+ reversed.rotate_right(1);
+ };
Subpath {
- manipulator_groups: Subpath::reverse_manipulator_groups(&self.manipulator_groups),
+ manipulator_groups: reversed,
closed: self.closed,
}
}
@@ -121,7 +126,7 @@ impl Subpath {
/// The resulting Subpath will wind from the given `t1` to `t2`.
/// That means, if the value of `t1` > `t2`, it will cross the break between endpoints from `t1` to `t = 1 = 0` to `t2`.
/// If a path winding in the reverse direction is desired, call `trim` on the `Subpath` returned from `Subpath::reverse`.
- ///
+ ///
pub fn trim(&self, t1: SubpathTValue, t2: SubpathTValue) -> Subpath {
// Return a clone of the Subpath if it is not long enough to be a valid Bezier
if self.manipulator_groups.is_empty() {
@@ -278,6 +283,9 @@ impl Subpath {
/// Smooths a Subpath up to the first derivative, using a weighted averaged based on segment length.
/// The Subpath must be open, and contain no quadratic segments.
pub(crate) fn smooth_open_subpath(&mut self) {
+ if self.len() < 2 {
+ return;
+ }
for i in 1..self.len() - 1 {
let first_bezier = self.manipulator_groups[i - 1].to_bezier(&self.manipulator_groups[i]);
let second_bezier = self.manipulator_groups[i].to_bezier(&self.manipulator_groups[i + 1]);
@@ -326,17 +334,22 @@ impl Subpath {
}
/// Reduces the segments of the subpath into simple subcurves, then scales each subcurve a set `distance` away.
- /// The intersections of segments of the subpath are joined using the method specified by the `joint` argument.
- ///
- pub fn offset(&self, distance: f64, joint: Joint) -> Subpath {
+ /// The intersections of segments of the subpath are joined using the method specified by the `join` argument.
+ ///
+ pub fn offset(&self, distance: f64, join: Join) -> Subpath {
assert!(self.len_segments() > 1, "Cannot offset an empty Subpath.");
// An offset at a distance 0 from the curve is simply the same curve
- if distance == 0. {
+ // An offset of a single point is not defined
+ if distance == 0. || self.len() == 1 {
return self.clone();
}
- let mut subpaths = self.iter().map(|bezier| bezier.offset(distance)).collect::>>();
+ let mut subpaths = self
+ .iter()
+ .filter(|bezier| !bezier.is_point())
+ .map(|bezier| bezier.offset(distance))
+ .collect::>>();
let mut drop_common_point = vec![true; self.len()];
// Clip or join consecutive Subpaths
@@ -359,7 +372,7 @@ impl Subpath {
let angle = out_tangent.angle_between(in_tangent);
// The angle is concave. The Subpath overlap and must be clipped
- let mut apply_joint = true;
+ let mut apply_join = true;
if (angle > 0. && distance > 0.) || (angle < 0. && distance < 0.) {
// If the distance is large enough, there may still be no intersections. Also, if the angle is close enough to zero,
// subpath intersections may find no intersections. In this case, the points are likely close enough that we can approximate
@@ -367,16 +380,27 @@ impl Subpath {
if let Some((clipped_subpath1, clipped_subpath2)) = Subpath::clip_simple_subpaths(subpath1, subpath2) {
subpaths[i] = clipped_subpath1;
subpaths[j] = clipped_subpath2;
- apply_joint = false;
+ apply_join = false;
}
}
- // The angle is convex. The Subpath must be joined using the specified Joint type
- if apply_joint {
- match joint {
- Joint::Bevel => {
- drop_common_point[j] = false;
+ // The angle is convex. The Subpath must be joined using the specified join type
+ if apply_join {
+ drop_common_point[j] = false;
+ match join {
+ Join::Bevel => {}
+ Join::Miter => {
+ let miter_manipulator_group = subpaths[i].miter_line_join(&subpaths[j]);
+ if let Some(miter_manipulator_group) = miter_manipulator_group {
+ subpaths[i].manipulator_groups.push(miter_manipulator_group);
+ }
+ }
+ Join::Round => {
+ let (out_handle, round_point, in_handle) = subpaths[i].round_line_join(&subpaths[j], self.manipulator_groups[j].anchor);
+ let last_index = subpaths[i].manipulator_groups.len() - 1;
+ subpaths[i].manipulator_groups[last_index].out_handle = Some(out_handle);
+ subpaths[i].manipulator_groups.push(round_point.clone());
+ subpaths[j].manipulator_groups[0].in_handle = Some(in_handle);
}
- _ => unimplemented!(),
}
}
}
@@ -387,22 +411,35 @@ impl Subpath {
let in_tangent = self.get_segment(0).unwrap().tangent(TValue::Parametric(0.));
let angle = out_tangent.angle_between(in_tangent);
- let mut apply_joint = true;
+ let mut apply_join = true;
if (angle > 0. && distance > 0.) || (angle < 0. && distance < 0.) {
if let Some((clipped_subpath1, clipped_subpath2)) = Subpath::clip_simple_subpaths(&subpaths[subpaths.len() - 1], &subpaths[0]) {
// Merge the clipped subpaths
let last_index = subpaths.len() - 1;
subpaths[last_index] = clipped_subpath1;
subpaths[0] = clipped_subpath2;
- apply_joint = false;
+ apply_join = false;
}
}
- if apply_joint {
- match joint {
- Joint::Bevel => {
- drop_common_point[0] = false;
+ if apply_join {
+ drop_common_point[0] = false;
+ match join {
+ Join::Bevel => {}
+ Join::Miter => {
+ let last_subpath_index = subpaths.len() - 1;
+ let miter_manipulator_group = subpaths[last_subpath_index].miter_line_join(&subpaths[0]);
+ if let Some(miter_manipulator_group) = miter_manipulator_group {
+ subpaths[last_subpath_index].manipulator_groups.push(miter_manipulator_group);
+ }
+ }
+ Join::Round => {
+ let last_subpath_index = subpaths.len() - 1;
+ let (out_handle, round_point, in_handle) = subpaths[last_subpath_index].round_line_join(&subpaths[0], self.manipulator_groups[0].anchor);
+ let last_index = subpaths[last_subpath_index].manipulator_groups.len() - 1;
+ subpaths[last_subpath_index].manipulator_groups[last_index].out_handle = Some(out_handle);
+ subpaths[last_subpath_index].manipulator_groups.push(round_point);
+ subpaths[0].manipulator_groups[0].in_handle = Some(in_handle);
}
- _ => unimplemented!(),
}
}
}
@@ -428,34 +465,68 @@ impl Subpath {
Subpath::new(manipulator_groups, self.closed)
}
+ /// Helper function to combine the two offsets that make up an outline.
+ pub(crate) fn combine_outline(&self, other: &Subpath, cap: Cap) -> Subpath {
+ let mut result_manipulator_groups: Vec> = vec![];
+ result_manipulator_groups.extend_from_slice(self.manipulator_groups());
+ match cap {
+ Cap::Butt => {
+ result_manipulator_groups.extend_from_slice(other.manipulator_groups());
+ }
+ Cap::Round => {
+ let last_index = result_manipulator_groups.len() - 1;
+ let (out_handle, round_point, in_handle) = self.round_cap(other);
+ result_manipulator_groups[last_index].out_handle = Some(out_handle);
+ result_manipulator_groups.push(round_point);
+ result_manipulator_groups.extend_from_slice(&other.manipulator_groups);
+ result_manipulator_groups[last_index + 2].in_handle = Some(in_handle);
+
+ let last_index = result_manipulator_groups.len() - 1;
+ let (out_handle, round_point, in_handle) = other.round_cap(self);
+ result_manipulator_groups[last_index].out_handle = Some(out_handle);
+ result_manipulator_groups.push(round_point);
+ result_manipulator_groups[0].in_handle = Some(in_handle);
+ }
+ Cap::Square => {
+ let square_points = self.square_cap(other);
+ result_manipulator_groups.extend_from_slice(&square_points);
+ result_manipulator_groups.extend_from_slice(other.manipulator_groups());
+ let square_points = other.square_cap(self);
+ result_manipulator_groups.extend_from_slice(&square_points);
+ }
+ }
+ Subpath::new(result_manipulator_groups, true)
+ }
+
// TODO: Replace this return type with `Path`, once the `Path` data type has been created.
/// Outline returns a single closed subpath (if the original subpath was open) or two closed subpaths (if the original subpath was closed) that forms
/// an approximate outline around the subpath at a specified distance from the curve. Outline takes the following parameters:
/// - `distance` - The outline's distance from the curve.
- /// - `joint` - The joint type used to cap the endpoints of open bezier curves, and join successive subpath segments.
- ///
- pub fn outline(&self, distance: f64, joint: Joint) -> (Subpath, Option>) {
- let mut pos_offset = self.offset(distance, joint);
- let mut neg_offset = self.reverse().offset(distance, joint);
+ /// - `join` - The join type used to cap the endpoints of open bezier curves, and join successive subpath segments.
+ ///
+ pub fn outline(&self, distance: f64, join: Join, cap: Cap) -> (Subpath, Option>) {
+ let is_point = self.is_point();
+ let (pos_offset, neg_offset) = if is_point {
+ let point = self.manipulator_groups[0].anchor;
+ (
+ Subpath::new(vec![ManipulatorGroup::new_anchor(point + DVec2::NEG_Y * distance)], false),
+ Subpath::new(vec![ManipulatorGroup::new_anchor(point + DVec2::Y * distance)], false),
+ )
+ } else {
+ (self.offset(distance, join), self.reverse().offset(distance, join))
+ };
- if self.closed {
+ if self.closed && !is_point {
return (pos_offset, Some(neg_offset));
}
- match joint {
- Joint::Bevel => {
- pos_offset.manipulator_groups.append(&mut neg_offset.manipulator_groups);
- pos_offset.closed = true;
- (pos_offset, None)
- }
- _ => unimplemented!(),
- }
+ (pos_offset.combine_outline(&neg_offset, cap), None)
}
}
#[cfg(test)]
mod tests {
- use super::{ManipulatorGroup, Subpath};
+ use super::{Cap, Join, ManipulatorGroup, Subpath};
use crate::compare::{compare_points, compare_subpaths, compare_vec_of_points};
use crate::consts::MAX_ABSOLUTE_DIFFERENCE;
use crate::utils::{SubpathTValue, TValue};
@@ -509,6 +580,43 @@ mod tests {
subpath
}
+ #[test]
+ fn outline_with_single_point_segment() {
+ let subpath = Subpath::new(
+ vec![
+ ManipulatorGroup {
+ anchor: DVec2::new(20., 20.),
+ out_handle: Some(DVec2::new(10., 90.)),
+ in_handle: None,
+ id: EmptyId,
+ },
+ ManipulatorGroup {
+ anchor: DVec2::new(150., 40.),
+ out_handle: None,
+ in_handle: Some(DVec2::new(60., 40.)),
+ id: EmptyId,
+ },
+ ManipulatorGroup {
+ anchor: DVec2::new(150., 40.),
+ out_handle: Some(DVec2::new(40., 120.)),
+ in_handle: None,
+ id: EmptyId,
+ },
+ ManipulatorGroup {
+ anchor: DVec2::new(100., 100.),
+ out_handle: None,
+ in_handle: None,
+ id: EmptyId,
+ },
+ ],
+ false,
+ );
+
+ let outline = subpath.outline(10., crate::Join::Round, crate::Cap::Round).0;
+ assert!(outline.manipulator_groups.windows(2).all(|pair| !pair[0].anchor.abs_diff_eq(pair[1].anchor, MAX_ABSOLUTE_DIFFERENCE)));
+ assert_eq!(outline.closed(), true);
+ }
+
#[test]
fn split_an_open_subpath() {
let subpath = set_up_open_subpath();
@@ -628,9 +736,15 @@ mod tests {
let result = temporary.reverse();
let end = result.len();
- assert_eq!(temporary.manipulator_groups[0].anchor, result.manipulator_groups[end - 1].anchor);
- assert_eq!(temporary.manipulator_groups[0].in_handle, result.manipulator_groups[end - 1].out_handle);
- assert_eq!(temporary.manipulator_groups[0].out_handle, result.manipulator_groups[end - 1].in_handle);
+ // Second manipulator group on the temporary subpath should be the reflected version of the last in the result
+ assert_eq!(temporary.manipulator_groups[1].anchor, result.manipulator_groups[end - 1].anchor);
+ assert_eq!(temporary.manipulator_groups[1].in_handle, result.manipulator_groups[end - 1].out_handle);
+ assert_eq!(temporary.manipulator_groups[1].out_handle, result.manipulator_groups[end - 1].in_handle);
+
+ // The first manipulator group in both should be the reflected versions of each other
+ assert_eq!(temporary.manipulator_groups[0].anchor, result.manipulator_groups[0].anchor);
+ assert_eq!(temporary.manipulator_groups[0].in_handle, result.manipulator_groups[0].out_handle);
+ assert_eq!(temporary.manipulator_groups[0].out_handle, result.manipulator_groups[0].in_handle);
assert_eq!(subpath, result);
}
@@ -907,4 +1021,46 @@ mod tests {
assert!(result.manipulator_groups[0].out_handle.is_none());
assert_eq!(result.manipulator_groups.len(), 1);
}
+
+ #[test]
+ fn outline_single_point_circle() {
+ let ellipse: Subpath = Subpath::new_ellipse(DVec2::new(0., 0.), DVec2::new(50., 50.)).reverse();
+ let p = DVec2::new(25., 25.);
+
+ let subpath: Subpath = Subpath::from_anchors([p, p, p], false);
+ let outline_open = subpath.outline(25., Join::Bevel, Cap::Round);
+ assert_eq!(outline_open.0, ellipse);
+ assert_eq!(outline_open.1, None);
+
+ let subpath_closed: Subpath = Subpath::from_anchors([p, p, p], true);
+ let outline_closed = subpath_closed.outline(25., Join::Bevel, Cap::Round);
+ assert_eq!(outline_closed.0, ellipse);
+ assert_eq!(outline_closed.1, None);
+ }
+
+ #[test]
+ fn outline_single_point_square() {
+ let square: Subpath = 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 subpath: Subpath = Subpath::from_anchors([p, p, p], false);
+ let outline_open = subpath.outline(25., Join::Bevel, Cap::Square);
+ assert_eq!(outline_open.0, square);
+ assert_eq!(outline_open.1, None);
+
+ let subpath_closed: Subpath = Subpath::from_anchors([p, p, p], true);
+ let outline_closed = subpath_closed.outline(25., Join::Bevel, Cap::Square);
+ assert_eq!(outline_closed.0, square);
+ assert_eq!(outline_closed.1, None);
+ }
}
diff --git a/libraries/bezier-rs/src/utils.rs b/libraries/bezier-rs/src/utils.rs
index 9a097575..8567de74 100644
--- a/libraries/bezier-rs/src/utils.rs
+++ b/libraries/bezier-rs/src/utils.rs
@@ -1,4 +1,5 @@
use crate::consts::{MAX_ABSOLUTE_DIFFERENCE, MIN_SEPARATION_VALUE, STRICT_MAX_ABSOLUTE_DIFFERENCE};
+use crate::ManipulatorGroup;
use glam::{BVec2, DMat2, DVec2};
use std::f64::consts::PI;
@@ -29,12 +30,23 @@ pub enum SubpathTValue {
}
#[derive(Copy, Clone)]
-pub enum Joint {
- Miter,
+/// Enum to represent the join type between subpaths.
+/// As defined in SVG: https://www.w3.org/TR/SVG2/painting.html#LineJoin.
+pub enum Join {
Bevel,
+ Miter,
Round,
}
+#[derive(Copy, Clone)]
+/// Enum to represent the cap type at the ends of an outline
+/// As defined in SVG: https://www.w3.org/TR/SVG2/painting.html#LineCaps.
+pub enum Cap {
+ Butt,
+ Round,
+ Square,
+}
+
/// Helper to perform the computation of a and c, where b is the provided point on the curve.
/// Given the correct power of `t` and `(1-t)`, the computation is the same for quadratic and cubic cases.
/// Relevant derivation and the definitions of a, b, and c can be found in [the projection identity section](https://pomax.github.io/bezierinfo/#abc) of Pomax's bezier curve primer.
@@ -266,6 +278,31 @@ pub fn scale_point_from_origin(point: DVec2, origin: DVec2, should_flip_directio
scale_point_from_direction_vector(point, (origin - point).normalize(), should_flip_direction, distance)
}
+/// Computes the necessary details to form a circular join from `left` to `right`, along a circle around `center`.
+/// By default, the angle is assumed to be 180 degrees.
+pub fn compute_circular_subpath_details(
+ left: DVec2,
+ arc_point: DVec2,
+ right: DVec2,
+ center: DVec2,
+ angle: Option,
+) -> (DVec2, ManipulatorGroup, DVec2) {
+ let center_to_arc_point = arc_point - center;
+
+ // Based on https://pomax.github.io/bezierinfo/#circles_cubic
+ let handle_offset_factor = if let Some(angle) = angle { 4. / 3. * (angle / 4.).tan() } else { 0.551784777779014 };
+
+ (
+ left - (left - center).perp() * handle_offset_factor,
+ ManipulatorGroup::new(
+ arc_point,
+ Some(arc_point + center_to_arc_point.perp() * handle_offset_factor),
+ Some(arc_point - center_to_arc_point.perp() * handle_offset_factor),
+ ),
+ right + (right - center).perp() * handle_offset_factor,
+ )
+}
+
#[cfg(test)]
mod tests {
use super::*;
diff --git a/website/other/bezier-rs-demos/src/components/SubpathDemoPane.ts b/website/other/bezier-rs-demos/src/components/SubpathDemoPane.ts
index 50592655..7348ee51 100644
--- a/website/other/bezier-rs-demos/src/components/SubpathDemoPane.ts
+++ b/website/other/bezier-rs-demos/src/components/SubpathDemoPane.ts
@@ -1,6 +1,6 @@
import subpathFeatures, { SubpathFeatureKey } from "@graphite/features/subpath-features";
import { renderDemoPane } from "@graphite/utils/render";
-import { Demo, DemoPane, InputOption, SubpathDemoArgs } from "@graphite/utils/types";
+import { Demo, DemoPane, SubpathDemoArgs, SubpathInputOption } from "@graphite/utils/types";
class SubpathDemoPane extends HTMLElement implements DemoPane {
// Props
@@ -8,7 +8,7 @@ class SubpathDemoPane extends HTMLElement implements DemoPane {
name!: string;
- inputOptions!: InputOption[];
+ inputOptions!: SubpathInputOption[];
triggerOnMouseMove!: boolean;
@@ -62,7 +62,12 @@ class SubpathDemoPane extends HTMLElement implements DemoPane {
subpathDemo.setAttribute("triples", JSON.stringify(demo.triples));
subpathDemo.setAttribute("closed", String(demo.closed));
subpathDemo.setAttribute("key", this.key);
- subpathDemo.setAttribute("inputOptions", JSON.stringify(this.inputOptions));
+
+ const inputOptions = this.inputOptions.map((option) => ({
+ ...option,
+ disabled: option.isDisabledForClosed && demo.closed,
+ }));
+ subpathDemo.setAttribute("inputOptions", JSON.stringify(inputOptions));
subpathDemo.setAttribute("triggerOnMouseMove", String(this.triggerOnMouseMove));
return subpathDemo;
}
diff --git a/website/other/bezier-rs-demos/src/features/bezier-features.ts b/website/other/bezier-rs-demos/src/features/bezier-features.ts
index 2a798351..cac89df6 100644
--- a/website/other/bezier-rs-demos/src/features/bezier-features.ts
+++ b/website/other/bezier-rs-demos/src/features/bezier-features.ts
@@ -1,5 +1,5 @@
import { WasmBezier } from "@graphite/../wasm/pkg";
-import { tSliderOptions, bezierTValueVariantOptions, errorOptions, minimumSeparationOptions } from "@graphite/utils/options";
+import { capOptions, tSliderOptions, bezierTValueVariantOptions, errorOptions, minimumSeparationOptions } from "@graphite/utils/options";
import { BezierDemoOptions, WasmBezierInstance, BezierCallback, InputOption, BEZIER_T_VALUE_VARIANTS } from "@graphite/utils/types";
const bezierFeatures = {
@@ -251,7 +251,7 @@ const bezierFeatures = {
},
outline: {
name: "Outline",
- callback: (bezier: WasmBezierInstance, options: Record): string => bezier.outline(options.distance),
+ callback: (bezier: WasmBezierInstance, options: Record): string => bezier.outline(options.distance, options.cap),
demoOptions: {
Quadratic: {
inputOptions: [
@@ -262,13 +262,14 @@ const bezierFeatures = {
step: 1,
default: 15,
},
+ capOptions,
],
},
},
},
"graduated-outline": {
name: "Graduated Outline",
- callback: (bezier: WasmBezierInstance, options: Record): string => bezier.graduated_outline(options.start_distance, options.end_distance),
+ callback: (bezier: WasmBezierInstance, options: Record): string => bezier.graduated_outline(options.start_distance, options.end_distance, options.cap),
demoOptions: {
Quadratic: {
inputOptions: [
@@ -286,6 +287,7 @@ const bezierFeatures = {
step: 1,
default: 15,
},
+ capOptions,
],
},
},
@@ -300,7 +302,8 @@ const bezierFeatures = {
},
"skewed-outline": {
name: "Skewed Outline",
- callback: (bezier: WasmBezierInstance, options: Record): string => bezier.skewed_outline(options.distance1, options.distance2, options.distance3, options.distance4),
+ callback: (bezier: WasmBezierInstance, options: Record): string =>
+ bezier.skewed_outline(options.distance1, options.distance2, options.distance3, options.distance4, options.cap),
demoOptions: {
Quadratic: {
inputOptions: [
@@ -332,6 +335,7 @@ const bezierFeatures = {
step: 1,
default: 5,
},
+ capOptions,
],
},
},
diff --git a/website/other/bezier-rs-demos/src/features/subpath-features.ts b/website/other/bezier-rs-demos/src/features/subpath-features.ts
index 570cda07..9f9e7a39 100644
--- a/website/other/bezier-rs-demos/src/features/subpath-features.ts
+++ b/website/other/bezier-rs-demos/src/features/subpath-features.ts
@@ -1,5 +1,5 @@
-import { tSliderOptions, subpathTValueVariantOptions, intersectionErrorOptions, minimumSeparationOptions } from "@graphite/utils/options";
-import { InputOption, SubpathCallback, WasmSubpathInstance, SUBPATH_T_VALUE_VARIANTS } from "@graphite/utils/types";
+import { capOptions, joinOptions, tSliderOptions, subpathTValueVariantOptions, intersectionErrorOptions, minimumSeparationOptions } from "@graphite/utils/options";
+import { SubpathCallback, SubpathInputOption, WasmSubpathInstance, SUBPATH_T_VALUE_VARIANTS } from "@graphite/utils/types";
const subpathFeatures = {
constructor: {
@@ -107,7 +107,7 @@ const subpathFeatures = {
},
offset: {
name: "Offset",
- callback: (subpath: WasmSubpathInstance, options: Record): string => subpath.offset(options.distance),
+ callback: (subpath: WasmSubpathInstance, options: Record): string => subpath.offset(options.distance, options.join),
inputOptions: [
{
variable: "distance",
@@ -116,11 +116,12 @@ const subpathFeatures = {
step: 1,
default: 10,
},
+ joinOptions,
],
},
outline: {
name: "Outline",
- callback: (subpath: WasmSubpathInstance, options: Record): string => subpath.outline(options.distance),
+ callback: (subpath: WasmSubpathInstance, options: Record): string => subpath.outline(options.distance, options.join, options.cap),
inputOptions: [
{
variable: "distance",
@@ -129,6 +130,8 @@ const subpathFeatures = {
step: 1,
default: 10,
},
+ joinOptions,
+ { ...capOptions, isDisabledForClosed: true },
],
},
};
@@ -137,7 +140,7 @@ export type SubpathFeatureKey = keyof typeof subpathFeatures;
export type SubpathFeatureOptions = {
name: string;
callback: SubpathCallback;
- inputOptions?: InputOption[];
+ inputOptions?: SubpathInputOption[];
triggerOnMouseMove?: boolean;
};
export default subpathFeatures as Record;
diff --git a/website/other/bezier-rs-demos/src/utils/options.ts b/website/other/bezier-rs-demos/src/utils/options.ts
index 2503d740..83a6e8df 100644
--- a/website/other/bezier-rs-demos/src/utils/options.ts
+++ b/website/other/bezier-rs-demos/src/utils/options.ts
@@ -45,3 +45,17 @@ export const subpathTValueVariantOptions = {
inputType: "dropdown",
options: SUBPATH_T_VALUE_VARIANTS,
};
+
+export const joinOptions = {
+ variable: "join",
+ default: 0,
+ inputType: "dropdown",
+ options: ["Bevel", "Miter", "Round"],
+};
+
+export const capOptions = {
+ variable: "cap",
+ default: 0,
+ inputType: "dropdown",
+ options: ["Butt", "Round", "Square"],
+};
diff --git a/website/other/bezier-rs-demos/src/utils/render.ts b/website/other/bezier-rs-demos/src/utils/render.ts
index 17fdc6a9..a4f48661 100644
--- a/website/other/bezier-rs-demos/src/utils/render.ts
+++ b/website/other/bezier-rs-demos/src/utils/render.ts
@@ -43,6 +43,10 @@ export function renderDemo(demo: Demo): void {
selectInput.append(option);
});
+ if (inputOption.disabled) {
+ selectInput.disabled = true;
+ }
+
selectInput.addEventListener("change", (event: Event): void => {
demo.sliderData[inputOption.variable] = Number((event.target as HTMLInputElement).value);
demo.drawDemo(figure);
diff --git a/website/other/bezier-rs-demos/src/utils/types.ts b/website/other/bezier-rs-demos/src/utils/types.ts
index ed795753..76253e27 100644
--- a/website/other/bezier-rs-demos/src/utils/types.ts
+++ b/website/other/bezier-rs-demos/src/utils/types.ts
@@ -22,6 +22,10 @@ export type BezierDemoOptions = {
};
};
+export type SubpathInputOption = InputOption & {
+ isDisabledForClosed?: boolean;
+};
+
export type InputOption = {
variable: string;
min?: number;
@@ -31,6 +35,7 @@ export type InputOption = {
unit?: string | string[];
inputType?: "slider" | "dropdown";
options?: string[];
+ disabled?: boolean;
};
export function getCurveType(numPoints: number): BezierCurveType {
diff --git a/website/other/bezier-rs-demos/wasm/src/bezier.rs b/website/other/bezier-rs-demos/wasm/src/bezier.rs
index 6eb94533..e57a5cc6 100644
--- a/website/other/bezier-rs-demos/wasm/src/bezier.rs
+++ b/website/other/bezier-rs-demos/wasm/src/bezier.rs
@@ -1,4 +1,6 @@
use crate::svg_drawing::*;
+use crate::utils::parse_cap;
+
use bezier_rs::{ArcStrategy, ArcsOptions, Bezier, Identifier, ProjectionOptions, TValue};
use glam::DVec2;
use serde::{Deserialize, Serialize};
@@ -219,22 +221,25 @@ impl WasmBezier {
}
pub fn curvature(&self, raw_t: f64, t_variant: String) -> String {
- let bezier = self.get_bezier_path();
+ let mut content = self.get_bezier_path();
let t = parse_t_variant(&t_variant, raw_t);
- let radius = 1. / self.0.curvature(t);
- let normal_point = self.0.normal(t);
- let intersection_point = self.0.evaluate(t);
+ let curvature = self.0.curvature(t);
+ if curvature > 0. {
+ let radius = 1. / self.0.curvature(t);
+ let normal_point = self.0.normal(t);
+ let intersection_point = self.0.evaluate(t);
- let curvature_center = intersection_point + normal_point * radius;
+ let curvature_center = intersection_point + normal_point * radius;
- let content = format!(
- "{bezier}{}{}{}{}",
- draw_circle(curvature_center, radius.abs(), RED, 1., NONE),
- draw_line(intersection_point.x, intersection_point.y, curvature_center.x, curvature_center.y, RED, 1.),
- draw_circle(intersection_point, 3., RED, 1., WHITE),
- draw_circle(curvature_center, 3., RED, 1., WHITE),
- );
+ content = format!(
+ "{content}{}{}{}{}",
+ draw_circle(curvature_center, radius.abs(), RED, 1., NONE),
+ draw_line(intersection_point.x, intersection_point.y, curvature_center.x, curvature_center.y, RED, 1.),
+ draw_circle(intersection_point, 3., RED, 1., WHITE),
+ draw_circle(curvature_center, 3., RED, 1., WHITE),
+ );
+ }
wrap_svg_tag(content)
}
@@ -242,19 +247,10 @@ impl WasmBezier {
let t = parse_t_variant(&t_variant, raw_t);
let beziers: [Bezier; 2] = self.0.split(t);
- let mut original_bezier_svg = String::new();
- self.0.to_svg(
- &mut original_bezier_svg,
- CURVE_ATTRIBUTES.to_string().replace(BLACK, WHITE),
- ANCHOR_ATTRIBUTES.to_string().replace(BLACK, WHITE),
- HANDLE_ATTRIBUTES.to_string(),
- HANDLE_LINE_ATTRIBUTES.to_string(),
- );
-
let mut bezier_svg_1 = String::new();
beziers[0].to_svg(
&mut bezier_svg_1,
- CURVE_ATTRIBUTES.to_string().replace(BLACK, ORANGE),
+ CURVE_ATTRIBUTES.to_string().replace(BLACK, ORANGE).replace("stroke-width=\"2\"", "stroke-width=\"8\"") + " opacity=\"0.5\"",
ANCHOR_ATTRIBUTES.to_string().replace(BLACK, ORANGE),
HANDLE_ATTRIBUTES.to_string().replace(GRAY, ORANGE),
HANDLE_LINE_ATTRIBUTES.to_string().replace(GRAY, ORANGE),
@@ -263,13 +259,13 @@ impl WasmBezier {
let mut bezier_svg_2 = String::new();
beziers[1].to_svg(
&mut bezier_svg_2,
- CURVE_ATTRIBUTES.to_string().replace(BLACK, RED),
+ CURVE_ATTRIBUTES.to_string().replace(BLACK, RED).replace("stroke-width=\"2\"", "stroke-width=\"8\"") + " opacity=\"0.5\"",
ANCHOR_ATTRIBUTES.to_string().replace(BLACK, RED),
HANDLE_ATTRIBUTES.to_string().replace(GRAY, RED),
HANDLE_LINE_ATTRIBUTES.to_string().replace(GRAY, RED),
);
- wrap_svg_tag(format!("{original_bezier_svg}{bezier_svg_1}{bezier_svg_2}"))
+ wrap_svg_tag(format!("{}{bezier_svg_1}{bezier_svg_2}", self.get_bezier_path()))
}
pub fn trim(&self, raw_t1: f64, raw_t2: f64, t_variant: String) -> String {
@@ -279,7 +275,7 @@ impl WasmBezier {
let mut trimmed_bezier_svg = String::new();
trimmed_bezier.to_svg(
&mut trimmed_bezier_svg,
- CURVE_ATTRIBUTES.to_string().replace(BLACK, RED),
+ CURVE_ATTRIBUTES.to_string().replace(BLACK, RED).replace("stroke-width=\"2\"", "stroke-width=\"8\"") + " opacity=\"0.5\"",
ANCHOR_ATTRIBUTES.to_string().replace(BLACK, RED),
HANDLE_ATTRIBUTES.to_string().replace(GRAY, RED),
HANDLE_LINE_ATTRIBUTES.to_string().replace(GRAY, RED),
@@ -570,8 +566,9 @@ impl WasmBezier {
wrap_svg_tag(bezier_curves_svg)
}
- pub fn outline(&self, distance: f64) -> String {
- let outline_subpath = self.0.outline::(distance);
+ pub fn outline(&self, distance: f64, cap: i32) -> String {
+ let cap = parse_cap(cap);
+ let outline_subpath = self.0.outline::(distance, cap);
if outline_subpath.is_empty() {
return String::new();
}
@@ -583,8 +580,9 @@ impl WasmBezier {
wrap_svg_tag(format!("{bezier_svg}{outline_svg}"))
}
- pub fn graduated_outline(&self, start_distance: f64, end_distance: f64) -> String {
- let outline_subpath = self.0.graduated_outline::(start_distance, end_distance);
+ pub fn graduated_outline(&self, start_distance: f64, end_distance: f64, cap: i32) -> String {
+ let cap = parse_cap(cap);
+ let outline_subpath = self.0.graduated_outline::(start_distance, end_distance, cap);
if outline_subpath.is_empty() {
return String::new();
}
@@ -596,8 +594,9 @@ impl WasmBezier {
wrap_svg_tag(format!("{bezier_svg}{outline_svg}"))
}
- pub fn skewed_outline(&self, distance1: f64, distance2: f64, distance3: f64, distance4: f64) -> String {
- let outline_subpath = self.0.skewed_outline::(distance1, distance2, distance3, distance4);
+ pub fn skewed_outline(&self, distance1: f64, distance2: f64, distance3: f64, distance4: f64, cap: i32) -> String {
+ let cap = parse_cap(cap);
+ let outline_subpath = self.0.skewed_outline::(distance1, distance2, distance3, distance4, cap);
if outline_subpath.is_empty() {
return String::new();
}
diff --git a/website/other/bezier-rs-demos/wasm/src/lib.rs b/website/other/bezier-rs-demos/wasm/src/lib.rs
index 0ac7850c..7bb48e20 100644
--- a/website/other/bezier-rs-demos/wasm/src/lib.rs
+++ b/website/other/bezier-rs-demos/wasm/src/lib.rs
@@ -1,3 +1,4 @@
pub mod bezier;
pub mod subpath;
mod svg_drawing;
+mod utils;
diff --git a/website/other/bezier-rs-demos/wasm/src/subpath.rs b/website/other/bezier-rs-demos/wasm/src/subpath.rs
index 4c176914..d8d713a3 100644
--- a/website/other/bezier-rs-demos/wasm/src/subpath.rs
+++ b/website/other/bezier-rs-demos/wasm/src/subpath.rs
@@ -1,4 +1,5 @@
use crate::svg_drawing::*;
+use crate::utils::{parse_cap, parse_join};
use bezier_rs::{Bezier, ManipulatorGroup, ProjectionOptions, Subpath, SubpathTValue};
@@ -377,8 +378,9 @@ impl WasmSubpath {
wrap_svg_tag(format!("{}{}", self.to_default_svg(), trimmed_subpath_svg))
}
- pub fn offset(&self, distance: f64) -> String {
- let offset_subpath = self.0.offset(distance, bezier_rs::Joint::Bevel);
+ pub fn offset(&self, distance: f64, join: i32) -> String {
+ let join = parse_join(join);
+ let offset_subpath = self.0.offset(distance, join);
let mut offset_svg = String::new();
offset_subpath.to_svg(&mut offset_svg, CURVE_ATTRIBUTES.to_string().replace(BLACK, RED), String::new(), String::new(), String::new());
@@ -386,8 +388,10 @@ impl WasmSubpath {
wrap_svg_tag(format!("{}{offset_svg}", self.to_default_svg()))
}
- pub fn outline(&self, distance: f64) -> String {
- let (outline_piece1, outline_piece2) = self.0.outline(distance, bezier_rs::Joint::Bevel);
+ pub fn outline(&self, distance: f64, join: i32, cap: i32) -> String {
+ let join = parse_join(join);
+ let cap = parse_cap(cap);
+ let (outline_piece1, outline_piece2) = self.0.outline(distance, join, cap);
let mut outline_piece1_svg = String::new();
outline_piece1.to_svg(&mut outline_piece1_svg, CURVE_ATTRIBUTES.to_string().replace(BLACK, RED), String::new(), String::new(), String::new());
diff --git a/website/other/bezier-rs-demos/wasm/src/utils.rs b/website/other/bezier-rs-demos/wasm/src/utils.rs
new file mode 100644
index 00000000..58dc1329
--- /dev/null
+++ b/website/other/bezier-rs-demos/wasm/src/utils.rs
@@ -0,0 +1,19 @@
+use bezier_rs::{Cap, Join};
+
+pub fn parse_join(join: i32) -> Join {
+ match join {
+ 0 => Join::Bevel,
+ 1 => Join::Miter,
+ 2 => Join::Round,
+ _ => panic!("Unexpected Join value: '{}'", join),
+ }
+}
+
+pub fn parse_cap(cap: i32) -> Cap {
+ match cap {
+ 0 => Cap::Butt,
+ 1 => Cap::Round,
+ 2 => Cap::Square,
+ _ => panic!("Unexpected Cap value: '{}'", cap),
+ }
+}