diff --git a/libraries/bezier-rs/src/bezier/solvers.rs b/libraries/bezier-rs/src/bezier/solvers.rs index 09898be9..16c06a64 100644 --- a/libraries/bezier-rs/src/bezier/solvers.rs +++ b/libraries/bezier-rs/src/bezier/solvers.rs @@ -53,7 +53,7 @@ impl Bezier { } /// Returns the non-normalized vector representing the tangent at the point `t` along the curve. - fn non_normalized_tangent(&self, t: f64) -> DVec2 { + pub(crate) fn non_normalized_tangent(&self, t: f64) -> DVec2 { match self.handles { BezierHandles::Linear => self.end - self.start, _ => self.derivative().unwrap().evaluate(TValue::Parametric(t)), @@ -202,7 +202,7 @@ impl Bezier { /// Implementation of the algorithm to find curve intersections by iterating on bounding boxes. /// - `self_original_t_interval` - Used to identify the `t` values of the original parent of `self` that the current iteration is representing. /// - `other_original_t_interval` - Used to identify the `t` values of the original parent of `other` that the current iteration is representing. - fn intersections_between_subcurves(&self, self_original_t_interval: Range, other: &Bezier, other_original_t_interval: Range, error: f64) -> Vec<[f64; 2]> { + pub(crate) fn intersections_between_subcurves(&self, self_original_t_interval: Range, other: &Bezier, other_original_t_interval: Range, error: f64) -> Vec<[f64; 2]> { let bounding_box1 = self.bounding_box(); let bounding_box2 = other.bounding_box(); @@ -245,8 +245,8 @@ impl Bezier { // TODO: Use an `impl Iterator` return type instead of a `Vec` /// Returns a list of filtered parametric `t` values that correspond to intersection points between the current bezier curve and the provided one - /// such that the difference between adjacent `t` values in sorted order is greater than some minimum seperation value. If the difference - /// between 2 adjacent `t` values is lesss than the minimum difference, the filtering takes the larger `t` value and discards the smaller `t` value. + /// such that the difference between adjacent `t` values in sorted order is greater than some minimum separation value. If the difference + /// between 2 adjacent `t` values is less than the minimum difference, the filtering takes the larger `t` value and discards the smaller `t` value. /// The returned `t` values are with respect to the current bezier, not the provided parameter. /// If the provided curve is linear, then zero intersection points will be returned along colinear segments. /// - `error` - For intersections where the provided bezier is non-linear, `error` defines the threshold for bounding boxes to be considered an intersection point. @@ -259,7 +259,7 @@ impl Bezier { intersection_t_values.sort_by(|a, b| a.partial_cmp(b).unwrap()); intersection_t_values.iter().fold(Vec::new(), |mut accumulator, t| { - if !accumulator.is_empty() && (accumulator.last().unwrap() - t).abs() < minimum_seperation.unwrap_or(MIN_SEPERATION_VALUE) { + if !accumulator.is_empty() && (accumulator.last().unwrap() - t).abs() < minimum_seperation.unwrap_or(MIN_SEPARATION_VALUE) { accumulator.pop(); } accumulator.push(*t); diff --git a/libraries/bezier-rs/src/bezier/transform.rs b/libraries/bezier-rs/src/bezier/transform.rs index 599ff202..7b3f09cd 100644 --- a/libraries/bezier-rs/src/bezier/transform.rs +++ b/libraries/bezier-rs/src/bezier/transform.rs @@ -1,11 +1,41 @@ use super::*; + +use crate::compare::compare_points; use crate::utils::{f64_compare, TValue}; +use crate::{AppendType, ManipulatorGroup, Subpath}; use glam::DMat2; use std::f64::consts::PI; /// Functionality that transform Beziers, such as split, reduce, offset, etc. impl Bezier { + /// Returns a linear approximation of the given [Bezier]. For higher order [Bezier], this means simply dropping the handles. + pub fn to_linear(&self) -> Bezier { + Bezier::from_linear_dvec2(self.start(), self.end()) + } + + /// Returns a quadratic approximation of the given [Bezier]. For cubic Bezier, which typically cannot be represented by a single + /// quadratic segment, this function simply takes the average of the cubic handles to be the new quadratic handle. + pub fn to_quadratic(&self) -> Bezier { + let handle = match self.handles { + BezierHandles::Linear => self.start, + BezierHandles::Quadratic { handle } => handle, + BezierHandles::Cubic { handle_start, handle_end } => (handle_start + handle_end) / 2., + }; + Bezier::from_quadratic_dvec2(self.start, handle, self.end) + } + + /// Returns a cubic approximation of the given [Bezier]. + pub fn to_cubic(&self) -> Bezier { + let (handle_start, handle_end) = match self.handles { + BezierHandles::Linear => (self.start, self.end), + // Conversion reference source: https://stackoverflow.com/a/63059651/775283 + BezierHandles::Quadratic { handle } => (self.start + (2. / 3.) * (handle - self.start), self.end + (2. / 3.) * (handle - self.end)), + BezierHandles::Cubic { handle_start: _, handle_end: _ } => return *self, + }; + Bezier::from_cubic_dvec2(self.start, handle_start, handle_end, self.end) + } + /// Returns the pair of Bezier curves that result from splitting the original curve at the point `t` along the curve. /// pub fn split(&self, t: TValue) -> [Bezier; 2] { @@ -154,7 +184,11 @@ impl Bezier { let step_size = step_size.unwrap_or(DEFAULT_REDUCE_STEP_SIZE); - let extrema = self.get_extrema_t_list(); + let mut extrema = self.get_extrema_t_list(); + if let BezierHandles::Cubic { handle_start: _, handle_end: _ } = self.handles { + extrema.append(&mut self.inflections()); + extrema.sort_by(|ex1, ex2| ex1.partial_cmp(ex2).unwrap()); + } // Split each subcurve such that each resulting segment is scalable. let mut result_beziers: Vec = Vec::new(); @@ -170,15 +204,6 @@ impl Bezier { result_t_values.push(t_subcurve_end); return; } - // According to , it is generally sufficient to split subcurves with no local extrema at `t = 0.5` to generate two scalable segments. - let [first_half, second_half] = subcurve.split(TValue::Parametric(0.5)); - if first_half.is_scalable() && second_half.is_scalable() { - result_beziers.push(first_half); - result_beziers.push(second_half); - result_t_values.push(t_subcurve_start + (t_subcurve_end - t_subcurve_start) / 2.); - result_t_values.push(t_subcurve_end); - return; - } // Greedily iterate across the subcurve at intervals of size `step_size` to break up the curve into maximally large segments let mut segment: Bezier; @@ -242,8 +267,14 @@ impl Bezier { // Find the intersection point of the endpoint normals let intersection = utils::line_intersection(self.start, normal_start, self.end, normal_end); + // If the Bezier is a quadratic, convert it to a cubic to increase expressiveness + let intermediate = match self.handles { + BezierHandles::Quadratic { handle: _ } => self.to_cubic(), + _ => *self, + }; + let should_flip_direction = (self.start - intersection).normalize().abs_diff_eq(normal_start, MAX_ABSOLUTE_DIFFERENCE); - self.apply_transformation(&|point| { + intermediate.apply_transformation(&|point| { let mut direction_unit_vector = (intersection - point).normalize(); if should_flip_direction { direction_unit_vector *= -1.; @@ -258,49 +289,47 @@ impl Bezier { pub fn graduated_scale(&self, start_distance: f64, end_distance: f64) -> Bezier { assert!(self.is_scalable(), "The curve provided to scale is not scalable. Reduce the curve first."); - let normal_start = self.normal(TValue::Parametric(0.)); - let normal_end = self.normal(TValue::Parametric(1.)); + // If the Bezier is a quadratic, convert it to a cubic to increase expressiveness + let intermediate = match self.handles { + BezierHandles::Quadratic { handle: _ } => self.to_cubic(), + _ => *self, + }; + + let normal_start = intermediate.normal(TValue::Parametric(0.)); + let normal_end = intermediate.normal(TValue::Parametric(1.)); // If normal unit vectors are equal, then the lines are parallel if normal_start.abs_diff_eq(normal_end, MAX_ABSOLUTE_DIFFERENCE) { - let transformed_start = utils::scale_point_from_direction_vector(self.start, self.normal(TValue::Parametric(0.)), false, start_distance); - let transformed_end = utils::scale_point_from_direction_vector(self.end, self.normal(TValue::Parametric(1.)), false, end_distance); + let transformed_start = utils::scale_point_from_direction_vector(intermediate.start, intermediate.normal(TValue::Parametric(0.)), false, start_distance); + let transformed_end = utils::scale_point_from_direction_vector(intermediate.end, intermediate.normal(TValue::Parametric(1.)), false, end_distance); - return match self.handles { + return match intermediate.handles { BezierHandles::Linear => Bezier::from_linear_dvec2(transformed_start, transformed_end), - BezierHandles::Quadratic { handle } => { - let handle_closest_t = self.project(handle, ProjectionOptions::default()); - let handle_scale_distance = (1. - handle_closest_t) * start_distance + handle_closest_t * end_distance; - let transformed_handle = utils::scale_point_from_direction_vector(handle, self.normal(TValue::Parametric(handle_closest_t)), false, handle_scale_distance); - Bezier::from_quadratic_dvec2(transformed_start, transformed_handle, transformed_end) - } + BezierHandles::Quadratic { handle: _ } => unreachable!(), BezierHandles::Cubic { handle_start, handle_end } => { - let handle_start_closest_t = self.project(handle_start, ProjectionOptions::default()); + let handle_start_closest_t = intermediate.project(handle_start, ProjectionOptions::default()); let handle_start_scale_distance = (1. - handle_start_closest_t) * start_distance + handle_start_closest_t * end_distance; - let transformed_handle_start = utils::scale_point_from_direction_vector(handle_start, self.normal(TValue::Parametric(handle_start_closest_t)), false, handle_start_scale_distance); + let transformed_handle_start = + utils::scale_point_from_direction_vector(handle_start, intermediate.normal(TValue::Parametric(handle_start_closest_t)), false, handle_start_scale_distance); - let handle_end_closest_t = self.project(handle_start, ProjectionOptions::default()); + let handle_end_closest_t = intermediate.project(handle_start, ProjectionOptions::default()); let handle_end_scale_distance = (1. - handle_end_closest_t) * start_distance + handle_end_closest_t * end_distance; - let transformed_handle_end = utils::scale_point_from_direction_vector(handle_end, self.normal(TValue::Parametric(handle_end_closest_t)), false, handle_end_scale_distance); + let transformed_handle_end = utils::scale_point_from_direction_vector(handle_end, intermediate.normal(TValue::Parametric(handle_end_closest_t)), false, handle_end_scale_distance); Bezier::from_cubic_dvec2(transformed_start, transformed_handle_start, transformed_handle_end, transformed_end) } }; } // Find the intersection point of the endpoint normals - let intersection = utils::line_intersection(self.start, normal_start, self.end, normal_end); - let should_flip_direction = (self.start - intersection).normalize().abs_diff_eq(normal_start, MAX_ABSOLUTE_DIFFERENCE); + let intersection = utils::line_intersection(intermediate.start, normal_start, intermediate.end, normal_end); + let should_flip_direction = (intermediate.start - intersection).normalize().abs_diff_eq(normal_start, MAX_ABSOLUTE_DIFFERENCE); - let transformed_start = utils::scale_point_from_origin(self.start, intersection, should_flip_direction, start_distance); - let transformed_end = utils::scale_point_from_origin(self.end, intersection, should_flip_direction, end_distance); + let transformed_start = utils::scale_point_from_origin(intermediate.start, intersection, should_flip_direction, start_distance); + let transformed_end = utils::scale_point_from_origin(intermediate.end, intersection, should_flip_direction, end_distance); - match self.handles { + match intermediate.handles { BezierHandles::Linear => Bezier::from_linear_dvec2(transformed_start, transformed_end), - BezierHandles::Quadratic { handle } => { - let handle_scale_distance = (start_distance + end_distance) / 2.; - let transformed_handle = utils::scale_point_from_origin(handle, intersection, should_flip_direction, handle_scale_distance); - Bezier::from_quadratic_dvec2(transformed_start, transformed_handle, transformed_end) - } + BezierHandles::Quadratic { handle: _ } => unreachable!(), BezierHandles::Cubic { handle_start, handle_end } => { let handle_start_scale_distance = (start_distance * 2. + end_distance) / 3.; let transformed_handle_start = utils::scale_point_from_origin(handle_start, intersection, should_flip_direction, handle_start_scale_distance); @@ -312,78 +341,107 @@ impl Bezier { } } - /// Offset will get all the reduceable subcurves, and for each subcurve, it will scale the subcurve a set distance away from the original curve. + /// Offset will break down the Bezier into reducible subcurves, and scale each subcurve a set distance from the original curve. /// Note that not all bezier curves are possible to offset, so this function first reduces the curve to scalable segments and then offsets those segments. /// A proof for why this is true can be found in the [Curve offsetting section](https://pomax.github.io/bezierinfo/#offsetting) of Pomax's bezier curve primer. /// Offset takes the following parameter: /// - `distance` - The offset's distance from the curve. Positive values will offset the curve in the same direction as the endpoint normals, /// while negative values will offset in the opposite direction. /// - pub fn offset(&self, distance: f64) -> Vec { - let mut reduced = self.reduce(None); - reduced.iter_mut().for_each(|bezier| *bezier = bezier.scale(distance)); - reduced + pub fn offset(&self, distance: f64) -> Subpath { + 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 the curve is not linear, smooth the handles. All segments produced by bezier::scale will be cubic. + if self.handles != BezierHandles::Linear { + scaled.smooth_open_subpath(); + } + + scaled } /// Version of the `offset` function which scales the offset such that the start of the offset is `start_distance` from the original curve, while the end of /// of the offset is `end_distance` from the original curve. The curve transitions from `start_distance` to `end_distance` gradually, proportional to the - /// distance along the equation (`t`-value) of the curve. Similarily to the `offset` function, the returned result is an approximation. - pub fn graduated_offset(&self, start_distance: f64, end_distance: f64) -> Vec { + /// distance along the equation (`t`-value) of the curve. Similarly to the `offset` function, the returned result is an approximation. + pub fn graduated_offset(&self, start_distance: f64, end_distance: f64) -> Subpath { let reduced = self.reduce(None); let mut next_start_distance = start_distance; let distance_difference = end_distance - start_distance; let total_length = self.length(None); - let mut result = vec![]; - reduced.iter().for_each(|bezier| { + let 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; - result.push(bezier.graduated_scale(next_start_distance, next_end_distance)); + let scaled_bezier = bezier.graduated_scale(next_start_distance, next_end_distance); + + if index > 0 && !compare_points(bezier.start(), reduced[index - 1].end()) { + result.append_bezier(&scaled_bezier, AppendType::SmoothJoin(MAX_ABSOLUTE_DIFFERENCE)); + } else { + result.append_bezier(&scaled_bezier, AppendType::IgnoreStart); + } next_start_distance = next_end_distance; }); + + // If the curve is not linear, smooth the handles. All segments produced by bezier::scale will be cubic. + if self.handles != BezierHandles::Linear { + result.smooth_open_subpath(); + } + result } /// Outline will return a vector of Beziers that creates an outline around the curve at the designated distance away from the curve. /// It makes use of the `offset` function, thus restrictions applicable to `offset` are relevant to this function as well. /// The 'caps', the linear segments at opposite ends of the outline, intersect the original curve at the midpoint of the cap. - /// /// Outline takes the following parameter: /// - `distance` - The outline's distance from the curve. /// - pub fn outline(&self, distance: f64) -> Vec { + pub fn outline(&self, distance: f64) -> Subpath { let first_segment = self.offset(distance); let third_segment = self.reverse().offset(distance); if first_segment.is_empty() || third_segment.is_empty() { - return vec![]; + return Subpath::new(vec![], false); } - let second_segment = Bezier::from_linear_dvec2(first_segment.last().unwrap().end, third_segment.first().unwrap().start); - let fourth_segment = Bezier::from_linear_dvec2(third_segment.last().unwrap().end, first_segment.first().unwrap().start); - [first_segment, vec![second_segment], third_segment, vec![fourth_segment]].concat() + 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) } /// Version of the `outline` function which draws the outline at the specified distances away from the curve. /// The outline begins `start_distance` away, and gradually move to being `end_distance` away. /// - pub fn graduated_outline(&self, start_distance: f64, end_distance: f64) -> Vec { + pub fn graduated_outline(&self, start_distance: f64, end_distance: f64) -> Subpath { self.skewed_outline(start_distance, end_distance, end_distance, start_distance) } /// Version of the `graduated_outline` function that allows for the 4 corners of the outline to be different distances away from the curve. /// - pub fn skewed_outline(&self, distance1: f64, distance2: f64, distance3: f64, distance4: f64) -> Vec { + 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); if first_segment.is_empty() || third_segment.is_empty() { - return vec![]; + return Subpath::new(vec![], false); } - let second_segment = Bezier::from_linear_dvec2(first_segment.last().unwrap().end, third_segment.first().unwrap().start); - let fourth_segment = Bezier::from_linear_dvec2(third_segment.last().unwrap().end, first_segment.first().unwrap().start); - [first_segment, vec![second_segment], third_segment, vec![fourth_segment]].concat() + 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) } /// Approximate a bezier curve with circular arcs. @@ -538,8 +596,9 @@ impl Bezier { #[cfg(test)] mod tests { use super::*; - use crate::compare::{compare_arcs, compare_vector_of_beziers}; + use crate::compare::{compare_arcs, compare_points, compare_vec_of_points}; use crate::utils::TValue; + use crate::EmptyId; #[test] fn test_split() { @@ -695,41 +754,103 @@ mod tests { 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!(compare_vector_of_beziers(&reduced_curves, expected_bezier_points)); + 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 + ))); // Check that the reduce helper is correct let (helper_curves, helper_t_values) = bezier.reduced_curves_and_t_values(None); - assert_eq!(&reduced_curves, &helper_curves); + assert!(reduced_curves + .iter() + .zip(helper_curves.iter()) + .all(|(bezier1, bezier2)| bezier1.abs_diff_eq(bezier2, MAX_ABSOLUTE_DIFFERENCE))); assert!(reduced_curves .iter() .zip(helper_t_values.windows(2)) .all(|(curve, t_pair)| curve.abs_diff_eq(&bezier.trim(TValue::Parametric(t_pair[0]), TValue::Parametric(t_pair[1])), MAX_ABSOLUTE_DIFFERENCE))) } - #[test] - fn test_offset() { - let p1 = DVec2::new(30., 50.); - let p2 = DVec2::new(140., 30.); - let p3 = DVec2::new(160., 170.); - let bezier1 = Bezier::from_quadratic_dvec2(p1, p2, p3); - let expected_bezier_points1 = vec![ - vec![DVec2::new(31.7888, 59.8387), DVec2::new(44.5924, 57.46446), DVec2::new(56.09375, 57.5)], - vec![DVec2::new(56.09375, 57.5), DVec2::new(94.94197, 56.5019), DVec2::new(117.6473, 84.5936)], - vec![DVec2::new(117.6473, 84.5936), DVec2::new(142.3985, 113.403), DVec2::new(150.1005, 171.4142)], - ]; - assert!(compare_vector_of_beziers(&bezier1.offset(10.), expected_bezier_points1)); + fn assert_valid_offset(bezier: &Bezier, offset: &Subpath, expected_distance: f64) { + // Verify that the offset is smooth + if offset.len() > 1 { + offset.iter().take(offset.len() - 2).zip(offset.iter().skip(1)).for_each(|beziers_pair| { + assert!(compare_points(beziers_pair.0.end, beziers_pair.1.start)); + assert!(compare_points(beziers_pair.0.normal(TValue::Parametric(1.)), beziers_pair.1.normal(TValue::Parametric(0.)))); + }); + } - let p4 = DVec2::new(32., 77.); - let p5 = DVec2::new(169., 25.); - let p6 = DVec2::new(164., 157.); - let bezier2 = Bezier::from_quadratic_dvec2(p4, p5, p6); - let expected_bezier_points2 = vec![ - vec![DVec2::new(42.6458, 105.04758), DVec2::new(75.0218, 91.9939), DVec2::new(98.09357, 92.3043)], - vec![DVec2::new(98.09357, 92.3043), DVec2::new(116.5995, 88.5479), DVec2::new(123.9055, 102.0401)], - vec![DVec2::new(123.9055, 102.0401), DVec2::new(136.6087, 116.9522), DVec2::new(134.1761, 147.9324)], - vec![DVec2::new(134.1761, 147.9324), DVec2::new(134.1812, 151.7987), DVec2::new(134.0215, 155.86445)], - ]; - assert!(compare_vector_of_beziers(&bezier2.offset(30.), expected_bezier_points2)); + // Verify that the offset spans the length of the curve + let start_distance = bezier.evaluate(TValue::Parametric(0.)).distance(offset.iter().next().unwrap().evaluate(TValue::Parametric(0.))); + assert!(f64_compare(start_distance, expected_distance, MAX_ABSOLUTE_DIFFERENCE)); + let end_distance = bezier.evaluate(TValue::Parametric(1.)).distance(offset.iter().last().unwrap().evaluate(TValue::Parametric(1.))); + assert!(f64_compare(end_distance, expected_distance, MAX_ABSOLUTE_DIFFERENCE)); + + let err_threshold = expected_distance / 10.; + // Sample the curve and verify that the offset lies at the correct distance from the curve. + // Collect the t-value associated with the point on the bezier closest to the sample. + let t_values: Vec = offset + .iter() + .flat_map(|offset_segment| { + [0.1, 0.25, 0.5, 0.75, 0.9] + .iter() + .map(|t| { + let offset_point = offset_segment.evaluate(TValue::Parametric(*t)); + let closest_point_t = bezier.project(offset_point, ProjectionOptions::default()); + let closest_point = bezier.evaluate(TValue::Parametric(closest_point_t)); + let actual_distance = offset_point.distance(closest_point); + + assert!(f64_compare(actual_distance, expected_distance, err_threshold)); + closest_point_t + }) + .collect::>() + }) + .collect(); + + // Verify that the curve segments are in the correct order by asserting that t_values is sorted + for i in 1..t_values.len() { + assert!(t_values[i - 1] < t_values[i]); + } + } + + #[test] + fn test_offset_linear() { + let start = DVec2::new(30., 60.); + let end = DVec2::new(140., 120.); + let bezier = Bezier::from_linear_dvec2(start, end); + + for distance in [-20., -10., 10., 20.] { + let offset = bezier.offset::(distance); + assert_valid_offset(&bezier, &offset, distance.abs()); + } + } + + #[test] + fn test_offset_quadratic() { + let start = DVec2::new(30., 50.); + let handle = DVec2::new(140., 30.); + let end = DVec2::new(160., 170.); + let bezier = Bezier::from_quadratic_dvec2(start, handle, end); + + for distance in [-20., -10., 10., 20.] { + let offset = bezier.offset::(distance); + assert_valid_offset(&bezier, &offset, distance.abs()); + } + } + + #[test] + fn test_offset_cubic() { + let start = DVec2::new(30., 30.); + let handle1 = DVec2::new(60., 140.); + let handle2 = DVec2::new(150., 30.); + let end = DVec2::new(160., 160.); + let bezier = Bezier::from_cubic_dvec2(start, handle1, handle2, end); + + for distance in [-20., -10., 10., 20.] { + let offset = bezier.offset::(distance); + assert_valid_offset(&bezier, &offset, distance.abs()); + } } #[test] @@ -737,29 +858,29 @@ mod tests { 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.); assert_eq!(outline.len(), 4); // Assert the first length-wise piece of the outline is 10 units from the line assert!(f64_compare( - outline[0].evaluate(TValue::Parametric(0.25)).distance(line.evaluate(TValue::Parametric(0.25))), + outline.iter().next().unwrap().evaluate(TValue::Parametric(0.25)).distance(line.evaluate(TValue::Parametric(0.25))), 10., MAX_ABSOLUTE_DIFFERENCE )); // f64 // Assert the first cap touches the line end point at the halfway point - assert!(outline[1].evaluate(TValue::Parametric(0.5)).abs_diff_eq(line.end(), MAX_ABSOLUTE_DIFFERENCE)); + assert!(outline.iter().nth(1).unwrap().evaluate(TValue::Parametric(0.5)).abs_diff_eq(line.end(), MAX_ABSOLUTE_DIFFERENCE)); // Assert the second length-wise piece of the outline is 10 units from the line assert!(f64_compare( - outline[2].evaluate(TValue::Parametric(0.25)).distance(line.evaluate(TValue::Parametric(0.75))), + outline.iter().nth(2).unwrap().evaluate(TValue::Parametric(0.25)).distance(line.evaluate(TValue::Parametric(0.75))), 10., MAX_ABSOLUTE_DIFFERENCE )); // f64 // Assert the second cap touches the line start point at the halfway point - assert!(outline[3].evaluate(TValue::Parametric(0.5)).abs_diff_eq(line.start(), MAX_ABSOLUTE_DIFFERENCE)); + assert!(outline.iter().nth(3).unwrap().evaluate(TValue::Parametric(0.5)).abs_diff_eq(line.start(), MAX_ABSOLUTE_DIFFERENCE)); } #[test] diff --git a/libraries/bezier-rs/src/compare.rs b/libraries/bezier-rs/src/compare.rs index df94d4fb..445f547e 100644 --- a/libraries/bezier-rs/src/compare.rs +++ b/libraries/bezier-rs/src/compare.rs @@ -1,11 +1,15 @@ /// Comparison functions used for tests in the bezier module -use super::{Bezier, CircleArc, Subpath}; -use crate::consts::MAX_ABSOLUTE_DIFFERENCE; +#[cfg(test)] +use super::{CircleArc, Subpath}; +#[cfg(test)] use crate::utils::f64_compare; +use crate::consts::MAX_ABSOLUTE_DIFFERENCE; + use glam::DVec2; // Compare two f64s with some maximum absolute difference to account for floating point errors +#[cfg(test)] pub fn compare_f64s(f1: f64, f2: f64) -> bool { f64_compare(f1, f2, MAX_ABSOLUTE_DIFFERENCE) } @@ -16,19 +20,13 @@ pub fn compare_points(p1: DVec2, p2: DVec2) -> bool { } /// Compare vectors of points by allowing some maximum absolute difference to account for floating point errors +#[cfg(test)] pub fn compare_vec_of_points(a: Vec, b: Vec, max_absolute_difference: f64) -> bool { a.len() == b.len() && a.into_iter().zip(b.into_iter()).all(|(p1, p2)| p1.abs_diff_eq(p2, max_absolute_difference)) } -/// Compare vectors of beziers by allowing some maximum absolute difference between points to account for floating point errors -pub fn compare_vector_of_beziers(beziers: &[Bezier], expected_bezier_points: Vec>) -> bool { - beziers - .iter() - .zip(expected_bezier_points.iter()) - .all(|(&a, b)| compare_vec_of_points(a.get_points().collect::>(), b.to_vec(), MAX_ABSOLUTE_DIFFERENCE)) -} - /// Compare circle arcs by allowing some maximum absolute difference between values to account for floating point errors +#[cfg(test)] pub fn compare_arcs(arc1: CircleArc, arc2: CircleArc) -> bool { compare_points(arc1.center, arc2.center) && f64_compare(arc1.radius, arc1.radius, MAX_ABSOLUTE_DIFFERENCE) @@ -38,6 +36,7 @@ pub fn compare_arcs(arc1: CircleArc, arc2: CircleArc) -> bool { /// Compare Subpath by verifying that their bezier segments match. /// In this way, matching quadratic segments where the handles are on opposite manipulator groups will be considered equal. +#[cfg(test)] pub fn compare_subpaths(subpath1: &Subpath, subpath2: &Subpath) -> bool { subpath1.len() == subpath2.len() && subpath1.closed() == subpath2.closed() && subpath1.iter().eq(subpath2.iter()) } diff --git a/libraries/bezier-rs/src/consts.rs b/libraries/bezier-rs/src/consts.rs index 5bdec44e..d7b18438 100644 --- a/libraries/bezier-rs/src/consts.rs +++ b/libraries/bezier-rs/src/consts.rs @@ -9,7 +9,7 @@ pub const NUM_DISTANCES: usize = 5; /// Maximum allowed angle that the normal of the `start` or `end` point can make with the normal of the corresponding handle for a curve to be considered scalable/simple. pub const SCALABLE_CURVE_MAX_ENDPOINT_NORMAL_ANGLE: f64 = std::f64::consts::PI / 3.; /// Minimum allowable separation between adjacent `t` values when calculating curve intersections -pub const MIN_SEPERATION_VALUE: f64 = 5. * 1e-3; +pub const MIN_SEPARATION_VALUE: f64 = 5. * 1e-3; /// Default error bound for `t_value_to_parametric` function when TValue argument is Euclidean pub const DEFAULT_EUCLIDEAN_ERROR_BOUND: f64 = 0.001; diff --git a/libraries/bezier-rs/src/lib.rs b/libraries/bezier-rs/src/lib.rs index fa95fbb5..61a7fd7b 100644 --- a/libraries/bezier-rs/src/lib.rs +++ b/libraries/bezier-rs/src/lib.rs @@ -1,5 +1,4 @@ //! Bezier-rs: A Bezier Math Library for Rust -#[cfg(test)] pub(crate) mod compare; mod bezier; @@ -9,4 +8,4 @@ mod utils; pub use bezier::*; pub use subpath::*; -pub use utils::{SubpathTValue, TValue}; +pub use utils::{Joint, SubpathTValue, TValue}; diff --git a/libraries/bezier-rs/src/subpath/core.rs b/libraries/bezier-rs/src/subpath/core.rs index 9347d77b..04b59242 100644 --- a/libraries/bezier-rs/src/subpath/core.rs +++ b/libraries/bezier-rs/src/subpath/core.rs @@ -14,7 +14,7 @@ impl Subpath { } /// Create a `Subpath` consisting of 2 manipulator groups from a `Bezier`. - pub fn from_bezier(bezier: Bezier) -> Self { + pub fn from_bezier(bezier: &Bezier) -> Self { Subpath::new( vec![ ManipulatorGroup { @@ -34,6 +34,47 @@ impl Subpath { ) } + /// Creates a subpath from a slice of [Bezier]. When two consecutive Beziers do not share an end and start point, this function + /// resolves the discrepancy by simply taking the start-point of the second Bezier as the anchor of the Manipulator Group. + pub fn from_beziers(beziers: &[Bezier], closed: bool) -> Self { + assert!(!closed || beziers.len() > 1, "A closed Subpath must contain at least 1 Bezier."); + if beziers.is_empty() { + return Subpath::new(vec![], closed); + } + + let first = beziers.first().unwrap(); + let mut manipulator_groups = vec![ManipulatorGroup { + anchor: first.start(), + in_handle: None, + out_handle: first.handle_start(), + id: ManipulatorGroupId::new(), + }]; + let mut inner_groups: Vec> = beziers + .windows(2) + .map(|bezier_pair| ManipulatorGroup { + anchor: bezier_pair[1].start(), + in_handle: bezier_pair[0].handle_end(), + out_handle: bezier_pair[1].handle_start(), + id: ManipulatorGroupId::new(), + }) + .collect::>>(); + manipulator_groups.append(&mut inner_groups); + + let last = beziers.last().unwrap(); + if !closed { + manipulator_groups.push(ManipulatorGroup { + anchor: last.end(), + in_handle: last.handle_end(), + out_handle: None, + id: ManipulatorGroupId::new(), + }); + return Subpath::new(manipulator_groups, false); + } + + manipulator_groups[0].in_handle = last.handle_end(); + Subpath::new(manipulator_groups, true) + } + /// Returns true if the `Subpath` contains no [ManipulatorGroup]. pub fn is_empty(&self) -> bool { self.manipulator_groups.is_empty() diff --git a/libraries/bezier-rs/src/subpath/manipulators.rs b/libraries/bezier-rs/src/subpath/manipulators.rs index 2efbfecb..bb07fbd7 100644 --- a/libraries/bezier-rs/src/subpath/manipulators.rs +++ b/libraries/bezier-rs/src/subpath/manipulators.rs @@ -34,6 +34,47 @@ impl Subpath { self.manipulator_groups[segment_index % number_of_groups].out_handle = first.handle_start(); self.manipulator_groups[(segment_index + 2) % number_of_groups].in_handle = second.handle_end(); } + + /// Append a [Bezier] to the end of a subpath from a vector of [Bezier]. + /// The `append_type` parameter determines how the function behaves when the subpath's last anchor is not equal to the Bezier's start point. + /// - `IgnoreStart`: drops the bezier's start point in favor of the subpath's last anchor + /// - `SmoothJoin(f64)`: joins the subpath's endpoint with the bezier's start with a another Bezier segment that is continuous up to the second derivative + /// if the difference between the subpath's end point and Bezier's start point exceeds the wrapped integer value. + /// This function assumes that the position of the [Bezier]'s starting point is equal to that of the Subpath's last manipulator group. + pub fn append_bezier(&mut self, bezier: &Bezier, append_type: AppendType) { + if self.manipulator_groups.is_empty() { + self.manipulator_groups = vec![ManipulatorGroup { + anchor: bezier.start(), + in_handle: None, + out_handle: None, + id: ManipulatorGroupId::new(), + }]; + } + let mut last_index = self.manipulator_groups.len() - 1; + let last_anchor = self.manipulator_groups[last_index].anchor; + + if let AppendType::SmoothJoin(max_absolute_difference) = append_type { + // If the provided Bezier does not start at a location similar to the end of the Subpath, + // add an additional manipulator group to represent a smooth join with a new bezier in between + if !last_anchor.abs_diff_eq(bezier.start(), max_absolute_difference) { + let last_bezier = if self.manipulator_groups.len() > 1 { + self.manipulator_groups[last_index - 1].to_bezier(&self.manipulator_groups[last_index]) + } else { + Bezier::from_linear_dvec2(last_anchor, last_anchor) + }; + let join_bezier = last_bezier.join(bezier); + self.append_bezier(&join_bezier, AppendType::IgnoreStart); + last_index = self.manipulator_groups.len() - 1; + } + } + self.manipulator_groups[last_index].out_handle = bezier.handle_start(); + self.manipulator_groups.push(ManipulatorGroup { + anchor: bezier.end(), + in_handle: bezier.handle_end(), + out_handle: None, + id: ManipulatorGroupId::new(), + }); + } } #[cfg(test)] diff --git a/libraries/bezier-rs/src/subpath/solvers.rs b/libraries/bezier-rs/src/subpath/solvers.rs index ccc1881a..f036ddf7 100644 --- a/libraries/bezier-rs/src/subpath/solvers.rs +++ b/libraries/bezier-rs/src/subpath/solvers.rs @@ -17,20 +17,27 @@ impl Subpath { /// Calculates the intersection points the subpath has with a given curve and returns a list of `(usize, f64)` tuples, /// where the `usize` represents the index of the curve in the subpath, and the `f64` represents the `t`-value local to /// that curve where the intersection occured. - /// This function expects the following: + /// Expects the following: /// - `other`: a [Bezier] curve to check intersections against /// - `error`: an optional f64 value to provide an error bound /// - `minimum_seperation`: 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_seperation: Option) -> Vec<(usize, f64)> { - // TODO: account for either euclidean or parametric type - let intersection_t_values: Vec<(usize, f64)> = self - .iter() + pub fn intersections(&self, other: &Bezier, error: Option, minimum_separation: Option) -> Vec<(usize, f64)> { + self.iter() .enumerate() - .flat_map(|(index, bezier)| bezier.intersections(other, error, minimum_seperation).into_iter().map(|t| (index, t)).collect::>()) - .collect(); + .flat_map(|(index, bezier)| bezier.intersections(other, error, minimum_separation).into_iter().map(|t| (index, t)).collect::>()) + .collect() + } + /// Calculates the intersection points the subpath has with another given subpath and returns a list of global parametric `t`-values. + /// 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 } diff --git a/libraries/bezier-rs/src/subpath/structs.rs b/libraries/bezier-rs/src/subpath/structs.rs index e9537496..e553cad0 100644 --- a/libraries/bezier-rs/src/subpath/structs.rs +++ b/libraries/bezier-rs/src/subpath/structs.rs @@ -93,3 +93,9 @@ impl ManipulatorGroup self.out_handle = self.out_handle.map(|out_handle| affine_transform.transform_point2(out_handle)); } } + +#[derive(Copy, Clone)] +pub enum AppendType { + IgnoreStart, + SmoothJoin(f64), +} diff --git a/libraries/bezier-rs/src/subpath/transform.rs b/libraries/bezier-rs/src/subpath/transform.rs index bd037117..2233b756 100644 --- a/libraries/bezier-rs/src/subpath/transform.rs +++ b/libraries/bezier-rs/src/subpath/transform.rs @@ -1,6 +1,8 @@ +use std::vec; + use super::*; -use crate::utils::SubpathTValue; -use crate::utils::TValue; +use crate::consts::MAX_ABSOLUTE_DIFFERENCE; +use crate::utils::{Joint, SubpathTValue, TValue}; use glam::DAffine2; @@ -235,7 +237,7 @@ impl Subpath { // If the target curve_indices are the same, then the trim must be happening within one bezier // This means curve1 == curve2 must be true, and we can simply call the Bezier trim. if t1_curve_index == t2_curve_index { - return Subpath::from_bezier(curve1.trim(TValue::Parametric(t1_curve_t), TValue::Parametric(t2_curve_t))); + return Subpath::from_bezier(&curve1.trim(TValue::Parametric(t1_curve_t), TValue::Parametric(t2_curve_t))); } // Split the bezier's with the according t value and keep the correct half @@ -272,6 +274,183 @@ impl Subpath { manipulator_group.apply_transform(affine_transform); } } + + /// 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) { + 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]); + if first_bezier.handle_end().is_none() || second_bezier.handle_end().is_none() { + continue; + } + let end_tangent = first_bezier.non_normalized_tangent(1.); + let start_tangent = second_bezier.non_normalized_tangent(0.); + + // Compute an average unit vector, weighing the segments by a rough estimation of their relative size. + let segment1_len = first_bezier.length(Some(5)); + let segment2_len = second_bezier.length(Some(5)); + let average_unit_tangent = (end_tangent.normalize() * segment1_len + start_tangent.normalize() * segment2_len) / (segment1_len + segment2_len); + + // Adjust start and end handles to fit the average tangent + let end_point = first_bezier.end(); + self.manipulator_groups[i].in_handle = Some((average_unit_tangent / 3. * -1.) * end_tangent.length() + end_point); + + let start_point = second_bezier.start(); + self.manipulator_groups[i].out_handle = Some((average_unit_tangent / 3.) * start_tangent.length() + start_point); + } + } + + // TODO: If a segment curls back on itself tightly enough it could intersect again at the portion that should be trimmed. This could cause the Subpaths to be clipped + // at the incorrect location. This can be avoided by first trimming the two Subpaths at any extrema, effectively ignoring loopbacks. + /// Helper function to clip overlap of two intersecting open Subpaths. Returns an optional, as intersections may not exist for certain arrangements and distances. + /// Assumes that the Subpaths represents simple Bezier segments, and clips the Subpaths at the last intersection of the first Subpath, and first intersection of the last Subpath. + fn clip_simple_subpaths(subpath1: &Subpath, subpath2: &Subpath) -> Option<(Subpath, Subpath)> { + // Split the first subpath at its last intersection + let intersections1 = subpath1.subpath_intersections(subpath2, None, None); + if intersections1.is_empty() { + return None; + } + let (segment_index, t) = *intersections1.last().unwrap(); + let (clipped_subpath1, _) = subpath1.split(SubpathTValue::Parametric { segment_index, t }); + + // Split the second subpath at its first intersection + let intersections2 = subpath2.subpath_intersections(subpath1, None, None); + if intersections2.is_empty() { + return None; + } + let (segment_index, t) = intersections2[0]; + let (_, clipped_subpath2) = subpath2.split(SubpathTValue::Parametric { segment_index, t }); + + Some((clipped_subpath1, clipped_subpath2.unwrap())) + } + + /// 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 { + 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. { + return self.clone(); + } + + let mut subpaths = self.iter().map(|bezier| bezier.offset(distance)).collect::>>(); + let mut drop_common_point = vec![true; self.len()]; + + // Clip or join consecutive Subpaths + for i in 0..subpaths.len() - 1 { + let j = i + 1; + let subpath1 = &subpaths[i]; + let subpath2 = &subpaths[j]; + + let last_segment = subpath1.get_segment(subpath1.len_segments() - 1).unwrap(); + let first_segment = subpath2.get_segment(0).unwrap(); + + // If the anchors are approximately equal, there is no need to clip / join the segments + if last_segment.end().abs_diff_eq(first_segment.start(), MAX_ABSOLUTE_DIFFERENCE) { + continue; + } + + // Calculate the angle formed between two consecutive Subpaths + let out_tangent = self.get_segment(i).unwrap().tangent(TValue::Parametric(1.)); + let in_tangent = self.get_segment(j).unwrap().tangent(TValue::Parametric(0.)); + let angle = out_tangent.angle_between(in_tangent); + + // The angle is concave. The Subpath overlap and must be clipped + let mut apply_joint = 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 + // the points as being on top of one another. + if let Some((clipped_subpath1, clipped_subpath2)) = Subpath::clip_simple_subpaths(subpath1, subpath2) { + subpaths[i] = clipped_subpath1; + subpaths[j] = clipped_subpath2; + apply_joint = 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; + } + _ => unimplemented!(), + } + } + } + + // Clip any overlap in the last segment + if self.closed { + let out_tangent = self.get_segment(self.len_segments() - 1).unwrap().tangent(TValue::Parametric(1.)); + 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; + 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; + } + } + if apply_joint { + match joint { + Joint::Bevel => { + drop_common_point[0] = false; + } + _ => unimplemented!(), + } + } + } + + // Merge the subpaths. Drop points which overlap with one another. + let mut manipulator_groups = subpaths[0].manipulator_groups.clone(); + for i in 1..subpaths.len() { + if drop_common_point[i] { + let last_group = manipulator_groups.pop().unwrap(); + let mut manipulators_copy = subpaths[i].manipulator_groups.clone(); + manipulators_copy[0].in_handle = last_group.in_handle; + + manipulator_groups.append(&mut manipulators_copy); + } else { + manipulator_groups.append(&mut subpaths[i].manipulator_groups.clone()); + } + } + if self.closed && drop_common_point[0] { + let last_group = manipulator_groups.pop().unwrap(); + manipulator_groups[0].in_handle = last_group.in_handle; + } + + Subpath::new(manipulator_groups, self.closed) + } + + // 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); + + if self.closed { + 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!(), + } + } } #[cfg(test)] @@ -515,7 +694,7 @@ mod tests { let result1 = subpath.trim(SubpathTValue::GlobalParametric(0.8), SubpathTValue::GlobalParametric(0.2)); let result2 = subpath.trim(SubpathTValue::GlobalParametric(0.2), SubpathTValue::GlobalParametric(0.8)); - assert!(compare_subpaths(&result1, &result2)); + assert!(compare_subpaths::(&result1, &result2)); } #[test] @@ -544,7 +723,7 @@ mod tests { let result = subpath.trim(SubpathTValue::GlobalParametric(0.), SubpathTValue::GlobalParametric(1.)); // Assume that resulting subpath would no longer have the any meaningless handles - let mut expected_subpath = subpath.clone(); + let mut expected_subpath = subpath; expected_subpath[3].out_handle = None; assert_eq!(result.manipulator_groups[0].anchor, location_front); @@ -561,7 +740,7 @@ mod tests { assert_eq!(result.manipulator_groups[0].anchor, location_front); assert_eq!(result.manipulator_groups[3].anchor, location_back); - assert!(compare_subpaths(&subpath, &result)); + assert!(compare_subpaths::(&subpath, &result)); } #[test] @@ -728,10 +907,4 @@ mod tests { assert!(result.manipulator_groups[0].out_handle.is_none()); assert_eq!(result.manipulator_groups.len(), 1); } - - fn transform_subpath() { - let mut subpath = set_up_open_subpath(); - subpath.apply_transform(glam::DAffine2::IDENTITY); - assert_eq!(subpath, set_up_open_subpath()); - } } diff --git a/libraries/bezier-rs/src/utils.rs b/libraries/bezier-rs/src/utils.rs index a8ecd78d..9a097575 100644 --- a/libraries/bezier-rs/src/utils.rs +++ b/libraries/bezier-rs/src/utils.rs @@ -1,4 +1,4 @@ -use crate::consts::{MAX_ABSOLUTE_DIFFERENCE, MIN_SEPERATION_VALUE, STRICT_MAX_ABSOLUTE_DIFFERENCE}; +use crate::consts::{MAX_ABSOLUTE_DIFFERENCE, MIN_SEPARATION_VALUE, STRICT_MAX_ABSOLUTE_DIFFERENCE}; use glam::{BVec2, DMat2, DVec2}; use std::f64::consts::PI; @@ -28,6 +28,13 @@ pub enum SubpathTValue { GlobalEuclideanWithinError { t: f64, error: f64 }, } +#[derive(Copy, Clone)] +pub enum Joint { + Miter, + Bevel, + Round, +} + /// 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. @@ -115,7 +122,7 @@ pub fn solve_reformatted_cubic(discriminant: f64, a: f64, p: f64, q: f64) -> Vec let a_divided_by_3 = a / 3.; let root_1 = 2. * cube_root(-q_divided_by_2) - a_divided_by_3; let root_2 = cube_root(q_divided_by_2) - a_divided_by_3; - if (root_1 - root_2).abs() > MIN_SEPERATION_VALUE { + if (root_1 - root_2).abs() > MIN_SEPARATION_VALUE { roots.push(root_1); } roots.push(root_2); 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 9c4b333e..d5066443 100644 --- a/website/other/bezier-rs-demos/src/features/bezier-features.ts +++ b/website/other/bezier-rs-demos/src/features/bezier-features.ts @@ -239,10 +239,10 @@ const bezierFeatures = { sliderOptions: [ { variable: "distance", - min: -50, - max: 50, + min: -30, + max: 30, step: 1, - default: 20, + default: 15, }, ], }, @@ -257,9 +257,9 @@ const bezierFeatures = { { variable: "distance", min: 0, - max: 50, + max: 30, step: 1, - default: 20, + default: 15, }, ], }, @@ -274,16 +274,16 @@ const bezierFeatures = { { variable: "start_distance", min: 0, - max: 50, + max: 30, step: 1, - default: 30, + default: 5, }, { variable: "end_distance", min: 0, - max: 50, + max: 30, step: 1, - default: 30, + default: 15, }, ], }, @@ -306,28 +306,28 @@ const bezierFeatures = { { variable: "distance1", min: 0, - max: 50, + max: 30, step: 1, default: 20, }, { variable: "distance2", min: 0, - max: 50, + max: 30, step: 1, default: 10, }, { variable: "distance3", min: 0, - max: 50, + max: 30, step: 1, default: 30, }, { variable: "distance4", min: 0, - max: 50, + max: 30, step: 1, default: 5, }, 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 32e703f6..f383abdd 100644 --- a/website/other/bezier-rs-demos/src/features/subpath-features.ts +++ b/website/other/bezier-rs-demos/src/features/subpath-features.ts @@ -114,6 +114,32 @@ const subpathFeatures = { ], chooseTVariant: true, }, + offset: { + name: "Offset", + callback: (subpath: WasmSubpathInstance, options: Record): string => subpath.offset(options.distance), + sliderOptions: [ + { + variable: "distance", + min: -25, + max: 25, + step: 1, + default: 10, + }, + ], + }, + outline: { + name: "Outline", + callback: (subpath: WasmSubpathInstance, options: Record): string => subpath.outline(options.distance), + sliderOptions: [ + { + variable: "distance", + min: 0, + max: 25, + step: 1, + default: 10, + }, + ], + }, }; export type SubpathFeatureKey = keyof typeof subpathFeatures; diff --git a/website/other/bezier-rs-demos/wasm/src/bezier.rs b/website/other/bezier-rs-demos/wasm/src/bezier.rs index e11999a9..6eb94533 100644 --- a/website/other/bezier-rs-demos/wasm/src/bezier.rs +++ b/website/other/bezier-rs-demos/wasm/src/bezier.rs @@ -1,5 +1,5 @@ use crate::svg_drawing::*; -use bezier_rs::{ArcStrategy, ArcsOptions, Bezier, ProjectionOptions, TValue}; +use bezier_rs::{ArcStrategy, ArcsOptions, Bezier, Identifier, ProjectionOptions, TValue}; use glam::DVec2; use serde::{Deserialize, Serialize}; use wasm_bindgen::prelude::*; @@ -49,6 +49,16 @@ fn parse_t_variant(t_variant: &String, t: f64) -> TValue { } } +/// An empty id type for use in tests +#[derive(Clone, Copy, PartialEq, Eq, Hash)] +pub(crate) struct EmptyId; + +impl Identifier for EmptyId { + fn new() -> Self { + Self + } +} + #[wasm_bindgen] impl WasmBezier { /// Expect js_points to be a list of 2 pairs. @@ -542,7 +552,7 @@ impl WasmBezier { let original_curve_svg = self.get_bezier_path(); let bezier_curves_svg = self .0 - .offset(distance) + .offset::(distance) .iter() .enumerate() .map(|(index, bezier_curve)| { @@ -561,36 +571,39 @@ impl WasmBezier { } pub fn outline(&self, distance: f64) -> String { - let outline_beziers = self.0.outline(distance); - if outline_beziers.is_empty() { + let outline_subpath = self.0.outline::(distance); + if outline_subpath.is_empty() { return String::new(); } - let outline_svg = draw_beziers(outline_beziers, CURVE_ATTRIBUTES.to_string().replace(BLACK, RED)); + let mut outline_svg = String::new(); + outline_subpath.to_svg(&mut outline_svg, CURVE_ATTRIBUTES.to_string().replace(BLACK, RED), String::new(), String::new(), String::new()); let bezier_svg = self.get_bezier_path(); wrap_svg_tag(format!("{bezier_svg}{outline_svg}")) } pub fn graduated_outline(&self, start_distance: f64, end_distance: f64) -> String { - let outline_beziers = self.0.graduated_outline(start_distance, end_distance); - if outline_beziers.is_empty() { + let outline_subpath = self.0.graduated_outline::(start_distance, end_distance); + if outline_subpath.is_empty() { return String::new(); } - let outline_svg = draw_beziers(outline_beziers, CURVE_ATTRIBUTES.to_string().replace(BLACK, RED)); + let mut outline_svg = String::new(); + outline_subpath.to_svg(&mut outline_svg, CURVE_ATTRIBUTES.to_string().replace(BLACK, RED), String::new(), String::new(), String::new()); let bezier_svg = self.get_bezier_path(); wrap_svg_tag(format!("{bezier_svg}{outline_svg}")) } pub fn skewed_outline(&self, distance1: f64, distance2: f64, distance3: f64, distance4: f64) -> String { - let outline_beziers = self.0.skewed_outline(distance1, distance2, distance3, distance4); - if outline_beziers.is_empty() { + let outline_subpath = self.0.skewed_outline::(distance1, distance2, distance3, distance4); + if outline_subpath.is_empty() { return String::new(); } - let outline_svg = draw_beziers(outline_beziers, CURVE_ATTRIBUTES.to_string().replace(BLACK, RED)); + let mut outline_svg = String::new(); + outline_subpath.to_svg(&mut outline_svg, CURVE_ATTRIBUTES.to_string().replace(BLACK, RED), String::new(), String::new(), String::new()); let bezier_svg = self.get_bezier_path(); wrap_svg_tag(format!("{bezier_svg}{outline_svg}")) diff --git a/website/other/bezier-rs-demos/wasm/src/subpath.rs b/website/other/bezier-rs-demos/wasm/src/subpath.rs index fe9aa741..ab6970e7 100644 --- a/website/other/bezier-rs-demos/wasm/src/subpath.rs +++ b/website/other/bezier-rs-demos/wasm/src/subpath.rs @@ -376,4 +376,29 @@ 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); + + 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()); + + 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); + + 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()); + + let mut outline_piece2_svg = String::new(); + if outline_piece2.is_some() { + outline_piece2 + .unwrap() + .to_svg(&mut outline_piece2_svg, CURVE_ATTRIBUTES.to_string().replace(BLACK, RED), String::new(), String::new(), String::new()); + } + + wrap_svg_tag(format!("{}{outline_piece1_svg}{outline_piece2_svg}", self.to_default_svg())) + } } diff --git a/website/other/bezier-rs-demos/wasm/src/svg_drawing.rs b/website/other/bezier-rs-demos/wasm/src/svg_drawing.rs index ef61e302..e7abdc12 100644 --- a/website/other/bezier-rs-demos/wasm/src/svg_drawing.rs +++ b/website/other/bezier-rs-demos/wasm/src/svg_drawing.rs @@ -1,6 +1,4 @@ -use bezier_rs::Bezier; use glam::DVec2; -use std::fmt::Write; // SVG drawing constants pub const SVG_OPEN_TAG: &str = r#""#; @@ -48,19 +46,6 @@ pub fn draw_line(start_x: f64, start_y: f64, end_x: f64, end_y: f64, stroke: &st format!(r#""#) } -/// Helper function to draw a list of beziers. -pub fn draw_beziers(beziers: Vec, options: String) -> String { - let start_point = beziers.first().unwrap().start(); - let mut svg = format!("", options); - svg -} - // Helper function to convert polar to cartesian coordinates fn polar_to_cartesian(center_x: f64, center_y: f64, radius: f64, angle_in_rad: f64) -> [f64; 2] { let x = center_x + radius * angle_in_rad.cos();