Bezier-rs: Add joins and caps to offsets and outlines (#1083)

* Intial work

* Improve miter and add round join

* Get arcs to go opposite direction

* Add cap and other refactors

* Rename joint to join, fix some bugs

* Fix single point issue

* Clean up

* Fix iframe sizes and update UI

* Address comments and handle single point outline

* Rename variables, fix branches in outline

* Address comments
This commit is contained in:
Hannah Li
2023-03-27 16:25:08 -04:00
committed by Keavon Chambers
parent 7e124c8035
commit c0576ab4e0
18 changed files with 684 additions and 173 deletions

View File

@@ -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)]

View File

@@ -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.
@@ -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<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

View File

@@ -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<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
}
@@ -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);
@@ -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];
@@ -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.
/// <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);
}
}
});
@@ -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.
/// <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.
@@ -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::<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);
@@ -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::<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);
@@ -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.);

View File

@@ -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};

View File

@@ -88,7 +88,7 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
/// 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<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
&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);

View File

@@ -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<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
/// Calculate the point on the subpath based on the parametric `t`-value provided.
@@ -22,7 +23,7 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
/// - `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.
/// <iframe frameBorder="0" width="100%" height="325px" src="https://graphite.rs/bezier-rs-demos#subpath/intersect-cubic/solo" title="Intersection Demo"></iframe>
/// <iframe frameBorder="0" width="100%" height="400px" src="https://graphite.rs/bezier-rs-demos#subpath/intersect-cubic/solo" title="Intersection Demo"></iframe>
pub fn intersections(&self, other: &Bezier, error: Option<f64>, minimum_separation: Option<f64>) -> Vec<(usize, f64)> {
self.iter()
.enumerate()
@@ -34,27 +35,20 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
/// This function expects the following:
/// - other: a [Bezier] curve to check intersections against
/// - error: an optional f64 value to provide an error bound
/// <iframe frameBorder="0" width="100%" height="325px" src="https://graphite.rs/bezier-rs-demos#subpath/intersect-cubic/solo" title="Intersection Demo"></iframe>
/// <iframe frameBorder="0" width="100%" height="400px" src="https://graphite.rs/bezier-rs-demos#subpath/intersect-cubic/solo" title="Intersection Demo"></iframe>
pub fn subpath_intersections(&self, other: &Subpath<ManipulatorGroupId>, error: Option<f64>, minimum_separation: Option<f64>) -> 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.
/// <iframe frameBorder="0" width="100%" height="400px" src="https://graphite.rs/bezier-rs-demos#subpath/tangent/solo" title="Tangent Demo"></iframe>
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.
/// <iframe frameBorder="0" width="100%" height="325px" src="https://graphite.rs/bezier-rs-demos#subpath/self-intersect/solo" title="Self-Intersection Demo"></iframe>
/// <iframe frameBorder="0" width="100%" height="350px" src="https://graphite.rs/bezier-rs-demos#subpath/self-intersect/solo" title="Self-Intersection Demo"></iframe>
pub fn self_intersections(&self, error: Option<f64>, minimum_separation: Option<f64>) -> Vec<(usize, f64)> {
let mut intersections_vec = Vec::new();
let err = error.unwrap_or(MAX_ABSOLUTE_DIFFERENCE);
@@ -74,6 +68,13 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
intersections_vec
}
/// Returns a normalized unit vector representing the tangent on the subpath based on the parametric `t`-value provided.
/// <iframe frameBorder="0" width="100%" height="400px" src="https://graphite.rs/bezier-rs-demos#subpath/tangent/solo" title="Tangent Demo"></iframe>
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.
/// <iframe frameBorder="0" width="100%" height="400px" src="https://graphite.rs/bezier-rs-demos#subpath/normal/solo" title="Normal Demo"></iframe>
pub fn normal(&self, t: SubpathTValue) -> DVec2 {
@@ -83,7 +84,7 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
/// 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]`.
/// <iframe frameBorder="0" width="100%" height="400px" src="https://graphite.rs/bezier-rs-demos#subpath/local-extrema/solo" title="Local Extrema Demo"></iframe>
/// <iframe frameBorder="0" width="100%" height="325px" src="https://graphite.rs/bezier-rs-demos#subpath/local-extrema/solo" title="Local Extrema Demo"></iframe>
pub fn local_extrema(&self) -> [Vec<f64>; 2] {
let number_of_curves = self.len_segments() as f64;
@@ -98,7 +99,7 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
}
/// Return the min and max corners that represent the bounding box of the subpath.
/// <iframe frameBorder="0" width="100%" height="400px" src="https://graphite.rs/bezier-rs-demos#subpath/bounding-box/solo" title="Bounding Box Demo"></iframe>
/// <iframe frameBorder="0" width="100%" height="325px" src="https://graphite.rs/bezier-rs-demos#subpath/bounding-box/solo" title="Bounding Box Demo"></iframe>
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<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
/// 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]`.
/// <iframe frameBorder="0" width="100%" height="400px" src="https://graphite.rs/bezier-rs-demos#subpath/inflections/solo" title="Inflections Demo"></iframe>
/// <iframe frameBorder="0" width="100%" height="300px" src="https://graphite.rs/bezier-rs-demos#subpath/inflections/solo" title="Inflections Demo"></iframe>
pub fn inflections(&self) -> Vec<f64> {
let number_of_curves = self.len_segments() as f64;
let inflection_t_values: Vec<f64> = self
@@ -135,6 +136,92 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
pub fn contains_point(&self, target_point: DVec2) -> bool {
self.iter().map(|bezier| bezier.winding(target_point)).sum::<i32>() != 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<ManipulatorGroupId>) -> Option<ManipulatorGroup<ManipulatorGroupId>> {
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<ManipulatorGroupId>, center: DVec2) -> (DVec2, ManipulatorGroup<ManipulatorGroupId>, 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<ManipulatorGroupId>) -> (DVec2, ManipulatorGroup<ManipulatorGroupId>, 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<ManipulatorGroupId>) -> [ManipulatorGroup<ManipulatorGroupId>; 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.);
}
}

View File

@@ -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<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
}
/// 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<ManipulatorGroupId> {
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<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
/// 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`.
/// <iframe frameBorder="0" width="100%" height="400px" src="https://graphite.rs/bezier-rs-demos#subpath/trim/solo" title="Trim Demo"></iframe>
/// <iframe frameBorder="0" width="100%" height="450px" src="https://graphite.rs/bezier-rs-demos#subpath/trim/solo" title="Trim Demo"></iframe>
pub fn trim(&self, t1: SubpathTValue, t2: SubpathTValue) -> Subpath<ManipulatorGroupId> {
// 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<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
/// 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<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
}
/// 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.
/// <iframe frameBorder="0" width="100%" height="375px" src="https://graphite.rs/bezier-rs-demos#subpath/offset/solo" title="Offset Demo"></iframe>
pub fn offset(&self, distance: f64, joint: Joint) -> Subpath<ManipulatorGroupId> {
/// The intersections of segments of the subpath are joined using the method specified by the `join` argument.
/// <iframe frameBorder="0" width="100%" height="400px" src="https://graphite.rs/bezier-rs-demos#subpath/offset/solo" title="Offset Demo"></iframe>
pub fn offset(&self, distance: f64, join: Join) -> Subpath<ManipulatorGroupId> {
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::<Vec<Subpath<ManipulatorGroupId>>>();
let mut subpaths = self
.iter()
.filter(|bezier| !bezier.is_point())
.map(|bezier| bezier.offset(distance))
.collect::<Vec<Subpath<ManipulatorGroupId>>>();
let mut drop_common_point = vec![true; self.len()];
// Clip or join consecutive Subpaths
@@ -359,7 +372,7 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
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<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
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<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
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<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
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<ManipulatorGroupId>, cap: Cap) -> Subpath<ManipulatorGroupId> {
let mut result_manipulator_groups: Vec<ManipulatorGroup<ManipulatorGroupId>> = 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.
/// <iframe frameBorder="0" width="100%" height="375px" src="https://graphite.rs/bezier-rs-demos#subpath/outline/solo" title="Outline Demo"></iframe>
pub fn outline(&self, distance: f64, joint: Joint) -> (Subpath<ManipulatorGroupId>, Option<Subpath<ManipulatorGroupId>>) {
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.
/// <iframe frameBorder="0" width="100%" height="450px" src="https://graphite.rs/bezier-rs-demos#subpath/outline/solo" title="Outline Demo"></iframe>
pub fn outline(&self, distance: f64, join: Join, cap: Cap) -> (Subpath<ManipulatorGroupId>, Option<Subpath<ManipulatorGroupId>>) {
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<EmptyId> = Subpath::new_ellipse(DVec2::new(0., 0.), DVec2::new(50., 50.)).reverse();
let p = DVec2::new(25., 25.);
let subpath: Subpath<EmptyId> = 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<EmptyId> = 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<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 subpath: Subpath<EmptyId> = 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<EmptyId> = 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);
}
}

View File

@@ -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<ManipulatorGroupId: crate::Identifier>(
left: DVec2,
arc_point: DVec2,
right: DVec2,
center: DVec2,
angle: Option<f64>,
) -> (DVec2, ManipulatorGroup<ManipulatorGroupId>, 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::*;