diff --git a/node-graph/gcore/src/vector/algorithms/bezpath_algorithms.rs b/node-graph/gcore/src/vector/algorithms/bezpath_algorithms.rs index 109ef31e8a..bbeac028e2 100644 --- a/node-graph/gcore/src/vector/algorithms/bezpath_algorithms.rs +++ b/node-graph/gcore/src/vector/algorithms/bezpath_algorithms.rs @@ -1,18 +1,20 @@ +use super::intersection::bezpath_intersections; use super::poisson_disk::poisson_disk_sample; +use super::util::segment_tangent; use crate::vector::algorithms::offset_subpath::MAX_ABSOLUTE_DIFFERENCE; -use crate::vector::misc::{PointSpacingType, dvec2_to_point}; -use glam::DVec2; +use crate::vector::misc::{PointSpacingType, dvec2_to_point, point_to_dvec2}; +use glam::{DMat2, DVec2}; use kurbo::{BezPath, CubicBez, DEFAULT_ACCURACY, Line, ParamCurve, ParamCurveDeriv, PathEl, PathSeg, Point, QuadBez, Rect, Shape}; +use std::f64::consts::{FRAC_PI_2, PI}; -/// Splits the [`BezPath`] at `t` value which lie in the range of [0, 1]. +/// Splits the [`BezPath`] at segment index at `t` value which lie in the range of [0, 1]. /// Returns [`None`] if the given [`BezPath`] has no segments or `t` is within f64::EPSILON of 0 or 1. -pub fn split_bezpath(bezpath: &BezPath, t: f64, euclidian: bool) -> Option<(BezPath, BezPath)> { +pub fn split_bezpath_at_segment(bezpath: &BezPath, segment_index: usize, t: f64) -> Option<(BezPath, BezPath)> { if t <= f64::EPSILON || (1. - t) <= f64::EPSILON || bezpath.segments().count() == 0 { return None; } // Get the segment which lies at the split. - let (segment_index, t) = t_value_to_parametric(bezpath, t, euclidian, None); let segment = bezpath.get_seg(segment_index + 1).unwrap(); // Divide the segment. @@ -53,7 +55,19 @@ pub fn split_bezpath(bezpath: &BezPath, t: f64, euclidian: bool) -> Option<(BezP Some((first_bezpath, second_bezpath)) } -pub fn position_on_bezpath(bezpath: &BezPath, t: f64, euclidian: bool, segments_length: Option<&[f64]>) -> Point { +/// Splits the [`BezPath`] at a `t` value which lies in the range of [0, 1]. +/// Returns [`None`] if the given [`BezPath`] has no segments or `t` is within f64::EPSILON of 0 or 1. +pub fn split_bezpath(bezpath: &BezPath, t: f64, euclidian: bool) -> Option<(BezPath, BezPath)> { + if t <= f64::EPSILON || (1. - t) <= f64::EPSILON || bezpath.segments().count() == 0 { + return None; + } + + // Get the segment which lies at the split. + let (segment_index, t) = t_value_to_parametric(bezpath, t, euclidian, None); + split_bezpath_at_segment(bezpath, segment_index, t) +} + +pub fn evaluate_bezpath(bezpath: &BezPath, t: f64, euclidian: bool, segments_length: Option<&[f64]>) -> Point { let (segment_index, t) = t_value_to_parametric(bezpath, t, euclidian, segments_length); bezpath.get_seg(segment_index + 1).unwrap().eval(t) } @@ -328,3 +342,117 @@ pub fn is_linear(segment: &PathSeg) -> bool { PathSeg::Cubic(CubicBez { p0, p1, p2, p3 }) => is_colinear(p0, p1, p3) && is_colinear(p0, p2, p3), } } + +// 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 +// TODO: 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 BezPaths. Returns an Option because intersections may not exist for certain arrangements and distances. +/// Assumes that the BezPaths represents simple Bezier segments, and clips the BezPaths at the last intersection of the first BezPath, and first intersection of the last BezPath. +pub fn clip_simple_bezpaths(bezpath1: &BezPath, bezpath2: &BezPath) -> Option<(BezPath, BezPath)> { + // Split the first subpath at its last intersection + let subpath_1_intersections = bezpath_intersections(bezpath1, bezpath2, None, None); + if subpath_1_intersections.is_empty() { + return None; + } + let (segment_index, t) = *subpath_1_intersections.last()?; + let (clipped_subpath1, _) = split_bezpath_at_segment(bezpath1, segment_index, t)?; + + // Split the second subpath at its first intersection + let subpath_2_intersections = bezpath_intersections(bezpath2, bezpath1, None, None); + if subpath_2_intersections.is_empty() { + return None; + } + let (segment_index, t) = subpath_2_intersections[0]; + let (_, clipped_subpath2) = split_bezpath_at_segment(bezpath2, segment_index, t)?; + + Some((clipped_subpath1, clipped_subpath2)) +} + +/// Returns the [`PathEl`] that is needed for a miter join if it is possible. +/// +/// `miter_limit` defines a limit for the ratio between the miter length and the stroke width. +/// Alternatively, this can be interpreted as limiting the angle that the miter can form. +/// When the limit is exceeded, no [`PathEl`] will be returned. +/// This value should be greater than 0. If not, the default of 4 will be used. +pub fn miter_line_join(bezpath1: &BezPath, bezpath2: &BezPath, miter_limit: Option) -> Option<[PathEl; 2]> { + let miter_limit = match miter_limit { + Some(miter_limit) if miter_limit > f64::EPSILON => miter_limit, + _ => 4., + }; + // TODO: Besides returning None using the `?` operator, is there a more appropriate way to handle a `None` result from `get_segment`? + let in_segment = bezpath1.segments().last()?; + let out_segment = bezpath2.segments().next()?; + + let in_tangent = segment_tangent(in_segment, 1.); + let out_tangent = segment_tangent(out_segment, 0.); + + if in_tangent == DVec2::ZERO || out_tangent == DVec2::ZERO { + // Avoid panic from normalizing zero vectors + // TODO: Besides returning None, is there a more appropriate way to handle this? + return None; + } + + let angle = (in_tangent * -1.).angle_to(out_tangent).abs(); + + if angle.to_degrees() < miter_limit { + return None; + } + + let p1 = in_segment.end(); + let p2 = point_to_dvec2(p1) + in_tangent.normalize(); + let line1 = Line::new(p1, dvec2_to_point(p2)); + + let p1 = out_segment.start(); + let p2 = point_to_dvec2(p1) + out_tangent.normalize(); + let line2 = Line::new(p1, dvec2_to_point(p2)); + + // If we don't find the intersection point to draw the miter join, we instead default to a bevel join. + // Otherwise, we return the element to create the join. + let intersection = line1.crossing_point(line2)?; + + Some([PathEl::LineTo(intersection), PathEl::LineTo(out_segment.start())]) +} + +/// Computes the [`PathEl`] to form a circular join from `left` to `right`, along a circle around `center`. +/// By default, the angle is assumed to be 180 degrees. +pub fn compute_circular_subpath_details(left: DVec2, arc_point: DVec2, right: DVec2, center: DVec2, angle: Option) -> [PathEl; 2] { + 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 }; + + let p1 = dvec2_to_point(left - (left - center).perp() * handle_offset_factor); + let p2 = dvec2_to_point(arc_point + center_to_arc_point.perp() * handle_offset_factor); + let p3 = dvec2_to_point(arc_point); + + let first_half = PathEl::CurveTo(p1, p2, p3); + + let p1 = dvec2_to_point(arc_point - center_to_arc_point.perp() * handle_offset_factor); + let p2 = dvec2_to_point(right + (right - center).perp() * handle_offset_factor); + let p3 = dvec2_to_point(right); + + let second_half = PathEl::CurveTo(p1, p2, p3); + + [first_half, second_half] +} + +/// Returns two [`PathEl`] to create a round join with the provided center. +pub fn round_line_join(bezpath1: &BezPath, bezpath2: &BezPath, center: DVec2) -> [PathEl; 2] { + let left = point_to_dvec2(bezpath1.segments().last().unwrap().end()); + let right = point_to_dvec2(bezpath2.segments().next().unwrap().start()); + + let center_to_right = right - center; + let center_to_left = left - center; + + let in_segment = bezpath1.segments().last(); + let in_tangent = in_segment.map(|in_segment| segment_tangent(in_segment, 1.)); + + let mut angle = center_to_right.angle_to(center_to_left) / 2.; + let mut arc_point = center + DMat2::from_angle(angle).mul_vec2(center_to_right); + + if in_tangent.map(|in_tangent| (arc_point - left).angle_to(in_tangent).abs()).unwrap_or_default() > FRAC_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)) +} diff --git a/node-graph/gcore/src/vector/algorithms/contants.rs b/node-graph/gcore/src/vector/algorithms/contants.rs new file mode 100644 index 0000000000..021e49be22 --- /dev/null +++ b/node-graph/gcore/src/vector/algorithms/contants.rs @@ -0,0 +1,6 @@ +/// Minimum allowable separation between adjacent `t` values when calculating curve intersections +pub const MIN_SEPARATION_VALUE: f64 = 5. * 1e-3; + +/// Constant used to determine if `f64`s are equivalent. +#[cfg(test)] +pub const MAX_ABSOLUTE_DIFFERENCE: f64 = 1e-3; diff --git a/node-graph/gcore/src/vector/algorithms/intersection.rs b/node-graph/gcore/src/vector/algorithms/intersection.rs new file mode 100644 index 0000000000..de373aa1b8 --- /dev/null +++ b/node-graph/gcore/src/vector/algorithms/intersection.rs @@ -0,0 +1,365 @@ +use super::contants::MIN_SEPARATION_VALUE; +use kurbo::{BezPath, DEFAULT_ACCURACY, ParamCurve, PathSeg, Shape}; + +/// Calculates the intersection points the bezpath has with a given segment and returns a list of `(usize, f64)` tuples, +/// where the `usize` represents the index of the segment in the bezpath, and the `f64` represents the `t`-value local to +/// that segment where the intersection occurred. +/// +/// `minimum_separation` is the minimum difference that two adjacent `t`-values must have when comparing adjacent `t`-values in sorted order. +pub fn bezpath_and_segment_intersections(bezpath: &BezPath, segment: PathSeg, accuracy: Option, minimum_separation: Option) -> Vec<(usize, f64)> { + bezpath + .segments() + .enumerate() + .flat_map(|(index, this_segment)| { + filtered_segment_intersections(this_segment, segment, accuracy, minimum_separation) + .into_iter() + .map(|t| (index, t)) + .collect::>() + }) + .collect() +} + +/// Calculates the intersection points the bezpath has with another given bezpath and returns a list of parametric `t`-values. +pub fn bezpath_intersections(bezpath1: &BezPath, bezpath2: &BezPath, accuracy: Option, minimum_separation: Option) -> Vec<(usize, f64)> { + let mut intersection_t_values: Vec<(usize, f64)> = bezpath2 + .segments() + .flat_map(|bezier| bezpath_and_segment_intersections(bezpath1, bezier, accuracy, minimum_separation)) + .collect(); + + intersection_t_values.sort_by(|a, b| a.partial_cmp(b).unwrap()); + intersection_t_values +} + +/// Calculates the intersection points the segment has with another given segment and returns a list of parametric `t`-values with given accuracy. +pub fn segment_intersections(segment1: PathSeg, segment2: PathSeg, accuracy: Option) -> Vec<(f64, f64)> { + let accuracy = accuracy.unwrap_or(DEFAULT_ACCURACY); + + match (segment1, segment2) { + (PathSeg::Line(line), segment2) => segment2.intersect_line(line).iter().map(|i| (i.line_t, i.segment_t)).collect(), + (segment1, PathSeg::Line(line)) => segment1.intersect_line(line).iter().map(|i| (i.segment_t, i.line_t)).collect(), + (segment1, segment2) => { + let mut intersections = Vec::new(); + segment_intersections_inner(segment1, 0., 1., segment2, 0., 1., accuracy, &mut intersections); + intersections + } + } +} + +/// Implements [https://pomax.github.io/bezierinfo/#curveintersection] to find intersection between two Bezier segments +/// by splitting the segment recursively until the size of the subsegment's bounding box is smaller than the accuracy. +#[allow(clippy::too_many_arguments)] +fn segment_intersections_inner(segment1: PathSeg, min_t1: f64, max_t1: f64, segment2: PathSeg, min_t2: f64, max_t2: f64, accuracy: f64, intersections: &mut Vec<(f64, f64)>) { + let bbox1 = segment1.bounding_box(); + let bbox2 = segment2.bounding_box(); + + let mid_t1 = (min_t1 + max_t1) / 2.; + let mid_t2 = (min_t2 + max_t2) / 2.; + + // Check if the bounding boxes overlap + if bbox1.overlaps(bbox2) { + // If bounding boxes overlap and they are small enough, we have found an intersection + if bbox1.width() < accuracy && bbox1.height() < accuracy && bbox2.width() < accuracy && bbox2.height() < accuracy { + // Use the middle `t` value, append the corresponding `t` value + intersections.push((mid_t1, mid_t2)); + return; + } + + // Split curves in half + let (seg11, seg12) = segment1.subdivide(); + let (seg21, seg22) = segment2.subdivide(); + + // Repeat checking the intersection with the combinations of the two halves of each curve + segment_intersections_inner(seg11, min_t1, mid_t1, seg21, min_t2, mid_t2, accuracy, intersections); + segment_intersections_inner(seg11, min_t1, mid_t1, seg22, mid_t2, max_t2, accuracy, intersections); + segment_intersections_inner(seg12, mid_t1, max_t1, seg21, min_t2, mid_t2, accuracy, intersections); + segment_intersections_inner(seg12, mid_t1, max_t1, seg22, mid_t2, max_t2, accuracy, intersections); + } +} + +// 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 segment and the provided one +/// 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 segment, not the provided parameter. +/// If the provided segment is linear, then zero intersection points will be returned along colinear segments. +/// +/// `accuracy` defines, for intersections where the provided bezier segment is non-linear, the maximum size of the bounding boxes to be considered an intersection point. +/// +/// `minimum_separation` is the minimum difference between adjacent `t` values in sorted order. +pub fn filtered_segment_intersections(segment1: PathSeg, segment2: PathSeg, accuracy: Option, minimum_separation: Option) -> Vec { + let mut intersection_t_values = segment_intersections(segment1, segment2, accuracy); + intersection_t_values.sort_by(|a, b| a.partial_cmp(b).unwrap()); + + intersection_t_values.iter().map(|x| x.0).fold(Vec::new(), |mut accumulator, t| { + if !accumulator.is_empty() && (accumulator.last().unwrap() - t).abs() < minimum_separation.unwrap_or(MIN_SEPARATION_VALUE) { + accumulator.pop(); + } + accumulator.push(t); + accumulator + }) +} + +// TODO: Use an `impl Iterator` return type instead of a `Vec` +/// Returns a list of pairs 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 separation value. If the difference between +/// two adjacent `t` values is less than the minimum difference, the filtering takes the larger `t` value and discards the smaller `t` value. +/// The first value in pair is with respect to the current bezier and the second value in pair is with respect to 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, defines the threshold for bounding boxes to be considered an intersection point. +/// +/// `minimum_separation` is the minimum difference between adjacent `t` values in sorted order +pub fn filtered_all_segment_intersections(segment1: PathSeg, segment2: PathSeg, accuracy: Option, minimum_separation: Option) -> Vec<(f64, f64)> { + let mut intersection_t_values = segment_intersections(segment1, segment2, accuracy); + intersection_t_values.sort_by(|a, b| (a.0 + a.1).partial_cmp(&(b.0 + b.1)).unwrap()); + + intersection_t_values.iter().fold(Vec::new(), |mut accumulator, t| { + if !accumulator.is_empty() + && (accumulator.last().unwrap().0 - t.0).abs() < minimum_separation.unwrap_or(MIN_SEPARATION_VALUE) + && (accumulator.last().unwrap().1 - t.1).abs() < minimum_separation.unwrap_or(MIN_SEPARATION_VALUE) + { + accumulator.pop(); + } + accumulator.push(*t); + accumulator + }) +} + +#[cfg(test)] +mod tests { + use super::{bezpath_and_segment_intersections, filtered_segment_intersections}; + use crate::vector::algorithms::{ + contants::MAX_ABSOLUTE_DIFFERENCE, + util::{compare_points, compare_vec_of_points, dvec2_compare}, + }; + + use kurbo::{BezPath, CubicBez, Line, ParamCurve, PathEl, PathSeg, Point, QuadBez}; + + #[test] + fn test_intersect_line_segment_quadratic() { + let p1 = Point::new(30., 50.); + let p2 = Point::new(140., 30.); + let p3 = Point::new(160., 170.); + + // Intersection at edge of curve + let bezier = PathSeg::Quad(QuadBez::new(p1, p2, p3)); + let line1 = PathSeg::Line(Line::new(Point::new(20., 50.), Point::new(40., 50.))); + let intersections1 = filtered_segment_intersections(bezier, line1, None, None); + assert!(intersections1.len() == 1); + assert!(compare_points(bezier.eval(intersections1[0]), p1)); + + // Intersection in the middle of curve + let line2 = PathSeg::Line(Line::new(Point::new(150., 150.), Point::new(30., 30.))); + let intersections2 = filtered_segment_intersections(bezier, line2, None, None); + assert!(compare_points(bezier.eval(intersections2[0]), Point::new(47.77355, 47.77354))); + } + + #[test] + fn test_intersect_curve_cubic_edge_case() { + // M34 107 C40 40 120 120 102 29 + + let p1 = Point::new(34., 107.); + let p2 = Point::new(40., 40.); + let p3 = Point::new(120., 120.); + let p4 = Point::new(102., 29.); + let cubic_segment = PathSeg::Cubic(CubicBez::new(p1, p2, p3, p4)); + + let linear_segment = PathSeg::Line(Line::new(Point::new(150., 150.), Point::new(20., 20.))); + let intersections = filtered_segment_intersections(cubic_segment, linear_segment, None, None); + + assert_eq!(intersections.len(), 1); + } + + #[test] + fn test_intersect_curve() { + let p0 = Point::new(30., 30.); + let p1 = Point::new(60., 140.); + let p2 = Point::new(150., 30.); + let p3 = Point::new(160., 160.); + + let cubic_segment = PathSeg::Cubic(CubicBez::new(p0, p1, p2, p3)); + + let p0 = Point::new(175., 140.); + let p1 = Point::new(20., 20.); + let p2 = Point::new(120., 20.); + + let quadratic_segment = PathSeg::Quad(QuadBez::new(p0, p1, p2)); + + let intersections1 = filtered_segment_intersections(cubic_segment, quadratic_segment, None, None); + let intersections2 = filtered_segment_intersections(quadratic_segment, cubic_segment, None, None); + + let intersections1_points: Vec = intersections1.iter().map(|&t| cubic_segment.eval(t)).collect(); + let intersections2_points: Vec = intersections2.iter().map(|&t| quadratic_segment.eval(t)).rev().collect(); + + assert!(compare_vec_of_points(intersections1_points, intersections2_points, 2.)); + } + + #[test] + fn intersection_linear_multiple_subpath_curves_test_one() { + // M 35 125 C 40 40 120 120 43 43 Q 175 90 145 150 Q 70 185 35 125 Z + + let cubic_start = Point::new(35., 125.); + let cubic_handle_1 = Point::new(40., 40.); + let cubic_handle_2 = Point::new(120., 120.); + let cubic_end = Point::new(43., 43.); + + let quadratic_1_handle = Point::new(175., 90.); + let quadratic_end = Point::new(145., 150.); + + let quadratic_2_handle = Point::new(70., 185.); + + let cubic_segment = PathSeg::Cubic(CubicBez::new(cubic_start, cubic_handle_1, cubic_handle_2, cubic_end)); + let quadratic_segment = PathSeg::Quad(QuadBez::new(cubic_end, quadratic_1_handle, quadratic_end)); + + let bezpath = BezPath::from_vec(vec![ + PathEl::MoveTo(cubic_start), + PathEl::CurveTo(cubic_handle_1, cubic_handle_2, cubic_end), + PathEl::QuadTo(quadratic_1_handle, quadratic_end), + PathEl::QuadTo(quadratic_2_handle, cubic_start), + PathEl::ClosePath, + ]); + + let linear_segment = PathSeg::Line(Line::new(Point::new(150., 150.), Point::new(20., 20.))); + + let cubic_intersections = filtered_segment_intersections(cubic_segment, linear_segment, None, None); + let quadratic_1_intersections = filtered_segment_intersections(quadratic_segment, linear_segment, None, None); + let bezpath_intersections = bezpath_and_segment_intersections(&bezpath, linear_segment, None, None); + + assert!( + dvec2_compare( + cubic_segment.eval(cubic_intersections[0]), + bezpath.segments().nth(bezpath_intersections[0].0).unwrap().eval(bezpath_intersections[0].1), + MAX_ABSOLUTE_DIFFERENCE + ) + .all() + ); + + assert!( + dvec2_compare( + quadratic_segment.eval(quadratic_1_intersections[0]), + bezpath.segments().nth(bezpath_intersections[1].0).unwrap().eval(bezpath_intersections[1].1), + MAX_ABSOLUTE_DIFFERENCE + ) + .all() + ); + + assert!( + dvec2_compare( + quadratic_segment.eval(quadratic_1_intersections[1]), + bezpath.segments().nth(bezpath_intersections[2].0).unwrap().eval(bezpath_intersections[2].1), + MAX_ABSOLUTE_DIFFERENCE + ) + .all() + ); + } + + #[test] + fn intersection_linear_multiple_subpath_curves_test_two() { + // M34 107 C40 40 120 120 102 29 Q175 90 129 171 Q70 185 34 107 Z + // M150 150 L 20 20 + + let cubic_start = Point::new(34., 107.); + let cubic_handle_1 = Point::new(40., 40.); + let cubic_handle_2 = Point::new(120., 120.); + let cubic_end = Point::new(102., 29.); + + let quadratic_1_handle = Point::new(175., 90.); + let quadratic_end = Point::new(129., 171.); + + let quadratic_2_handle = Point::new(70., 185.); + + let cubic_segment = PathSeg::Cubic(CubicBez::new(cubic_start, cubic_handle_1, cubic_handle_2, cubic_end)); + let quadratic_segment = PathSeg::Quad(QuadBez::new(cubic_end, quadratic_1_handle, quadratic_end)); + + let bezpath = BezPath::from_vec(vec![ + PathEl::MoveTo(cubic_start), + PathEl::CurveTo(cubic_handle_1, cubic_handle_2, cubic_end), + PathEl::QuadTo(quadratic_1_handle, quadratic_end), + PathEl::QuadTo(quadratic_2_handle, cubic_start), + PathEl::ClosePath, + ]); + + let line = PathSeg::Line(Line::new(Point::new(150., 150.), Point::new(20., 20.))); + + let cubic_intersections = filtered_segment_intersections(cubic_segment, line, None, None); + let quadratic_1_intersections = filtered_segment_intersections(quadratic_segment, line, None, None); + let bezpath_intersections = bezpath_and_segment_intersections(&bezpath, line, None, None); + + assert!( + dvec2_compare( + cubic_segment.eval(cubic_intersections[0]), + bezpath.segments().nth(bezpath_intersections[0].0).unwrap().eval(bezpath_intersections[0].1), + MAX_ABSOLUTE_DIFFERENCE + ) + .all() + ); + + assert!( + dvec2_compare( + quadratic_segment.eval(quadratic_1_intersections[0]), + bezpath.segments().nth(bezpath_intersections[1].0).unwrap().eval(bezpath_intersections[1].1), + MAX_ABSOLUTE_DIFFERENCE + ) + .all() + ); + } + + #[test] + fn intersection_linear_multiple_subpath_curves_test_three() { + // M35 125 C40 40 120 120 44 44 Q175 90 145 150 Q70 185 35 125 Z + + let cubic_start = Point::new(35., 125.); + let cubic_handle_1 = Point::new(40., 40.); + let cubic_handle_2 = Point::new(120., 120.); + let cubic_end = Point::new(44., 44.); + + let quadratic_1_handle = Point::new(175., 90.); + let quadratic_end = Point::new(145., 150.); + + let quadratic_2_handle = Point::new(70., 185.); + + let cubic_segment = PathSeg::Cubic(CubicBez::new(cubic_start, cubic_handle_1, cubic_handle_2, cubic_end)); + let quadratic_segment = PathSeg::Quad(QuadBez::new(cubic_end, quadratic_1_handle, quadratic_end)); + + let bezpath = BezPath::from_vec(vec![ + PathEl::MoveTo(cubic_start), + PathEl::CurveTo(cubic_handle_1, cubic_handle_2, cubic_end), + PathEl::QuadTo(quadratic_1_handle, quadratic_end), + PathEl::QuadTo(quadratic_2_handle, cubic_start), + PathEl::ClosePath, + ]); + + let line = PathSeg::Line(Line::new(Point::new(150., 150.), Point::new(20., 20.))); + + let cubic_intersections = filtered_segment_intersections(cubic_segment, line, None, None); + let quadratic_1_intersections = filtered_segment_intersections(quadratic_segment, line, None, None); + let bezpath_intersections = bezpath_and_segment_intersections(&bezpath, line, None, None); + + assert!( + dvec2_compare( + cubic_segment.eval(cubic_intersections[0]), + bezpath.segments().nth(bezpath_intersections[0].0).unwrap().eval(bezpath_intersections[0].1), + MAX_ABSOLUTE_DIFFERENCE + ) + .all() + ); + + assert!( + dvec2_compare( + quadratic_segment.eval(quadratic_1_intersections[0]), + bezpath.segments().nth(bezpath_intersections[1].0).unwrap().eval(bezpath_intersections[1].1), + MAX_ABSOLUTE_DIFFERENCE + ) + .all() + ); + + assert!( + dvec2_compare( + quadratic_segment.eval(quadratic_1_intersections[1]), + bezpath.segments().nth(bezpath_intersections[2].0).unwrap().eval(bezpath_intersections[2].1), + MAX_ABSOLUTE_DIFFERENCE + ) + .all() + ); + } +} diff --git a/node-graph/gcore/src/vector/algorithms/mod.rs b/node-graph/gcore/src/vector/algorithms/mod.rs index ed44f25590..b9284f327d 100644 --- a/node-graph/gcore/src/vector/algorithms/mod.rs +++ b/node-graph/gcore/src/vector/algorithms/mod.rs @@ -1,6 +1,9 @@ pub mod bezpath_algorithms; +mod contants; pub mod instance; +pub mod intersection; pub mod merge_by_distance; pub mod offset_subpath; pub mod poisson_disk; pub mod spline; +pub mod util; diff --git a/node-graph/gcore/src/vector/algorithms/offset_subpath.rs b/node-graph/gcore/src/vector/algorithms/offset_subpath.rs index 2041ebdefa..776a06a1db 100644 --- a/node-graph/gcore/src/vector/algorithms/offset_subpath.rs +++ b/node-graph/gcore/src/vector/algorithms/offset_subpath.rs @@ -1,173 +1,137 @@ -use crate::vector::PointId; -use bezier_rs::{Bezier, BezierHandles, Join, Subpath, TValue}; +use super::bezpath_algorithms::{clip_simple_bezpaths, miter_line_join, round_line_join}; +use crate::vector::misc::point_to_dvec2; +use kurbo::{BezPath, Join, ParamCurve, PathEl, PathSeg}; /// Value to control smoothness and mathematical accuracy to offset a cubic Bezier. const CUBIC_REGULARIZATION_ACCURACY: f64 = 0.5; /// Accuracy of fitting offset curve to Bezier paths. const CUBIC_TO_BEZPATH_ACCURACY: f64 = 1e-3; /// Constant used to determine if `f64`s are equivalent. -pub const MAX_ABSOLUTE_DIFFERENCE: f64 = 1e-3; +pub const MAX_ABSOLUTE_DIFFERENCE: f64 = 1e-7; -fn segment_to_bezier(seg: kurbo::PathSeg) -> Bezier { - match seg { - kurbo::PathSeg::Line(line) => Bezier::from_linear_coordinates(line.p0.x, line.p0.y, line.p1.x, line.p1.y), - kurbo::PathSeg::Quad(quad_bez) => Bezier::from_quadratic_coordinates(quad_bez.p0.x, quad_bez.p0.y, quad_bez.p1.x, quad_bez.p1.y, quad_bez.p1.x, quad_bez.p1.y), - kurbo::PathSeg::Cubic(cubic_bez) => Bezier::from_cubic_coordinates( - cubic_bez.p0.x, - cubic_bez.p0.y, - cubic_bez.p1.x, - cubic_bez.p1.y, - cubic_bez.p2.x, - cubic_bez.p2.y, - cubic_bez.p3.x, - cubic_bez.p3.y, - ), - } -} - -// TODO: Replace the implementation to use only Kurbo API. -/// Reduces the segments of the subpath into simple subcurves, then offset each subcurve a set `distance` away. +/// Reduces the segments of the bezpath into simple subcurves, then offset each subcurve a set `distance` away. /// The intersections of segments of the subpath are joined using the method specified by the `join` argument. -pub fn offset_subpath(subpath: &Subpath, distance: f64, join: Join) -> Subpath { +pub fn offset_bezpath(bezpath: &BezPath, distance: f64, join: Join, miter_limit: Option) -> BezPath { // An offset at a distance 0 from the curve is simply the same curve. // An offset of a single point is not defined. - if distance == 0. || subpath.len() <= 1 || subpath.len_segments() < 1 { - return subpath.clone(); + if distance == 0. || bezpath.get_seg(1).is_none() { + return bezpath.clone(); } - let mut subpaths = subpath - .iter() - .filter(|bezier| !bezier.is_point()) + let mut bezpaths = bezpath + .segments() .map(|bezier| bezier.to_cubic()) - .map(|cubic| { - let Bezier { start, end, handles } = cubic; - let BezierHandles::Cubic { handle_start, handle_end } = handles else { unreachable!()}; - - let cubic_bez = kurbo::CubicBez::new((start.x, start.y), (handle_start.x, handle_start.y), (handle_end.x, handle_end.y), (end.x, end.y)); + .map(|cubic_bez| { let cubic_offset = kurbo::offset::CubicOffset::new_regularized(cubic_bez, distance, CUBIC_REGULARIZATION_ACCURACY); - let offset_bezpath = kurbo::fit_to_bezpath(&cubic_offset, CUBIC_TO_BEZPATH_ACCURACY); - - let beziers = offset_bezpath.segments().fold(Vec::new(), |mut acc, seg| { - acc.push(segment_to_bezier(seg)); - acc - }); - Subpath::from_beziers(&beziers, false) + kurbo::fit_to_bezpath(&cubic_offset, CUBIC_TO_BEZPATH_ACCURACY) }) - .filter(|subpath| subpath.len() >= 2) // In some cases the reduced and scaled bézier is marked by is_point (so the subpath is empty). - .collect::>>(); - - let mut drop_common_point = vec![true; subpath.len()]; + .filter(|bezpath| bezpath.get_seg(1).is_some()) // In some cases the reduced and scaled bézier is marked by is_point (so the subpath is empty). + .collect::>(); // Clip or join consecutive Subpaths - for i in 0..subpaths.len() - 1 { + for i in 0..bezpaths.len() - 1 { let j = i + 1; - let subpath1 = &subpaths[i]; - let subpath2 = &subpaths[j]; + let bezpath1 = &bezpaths[i]; + let bezpath2 = &bezpaths[j]; - let last_segment = subpath1.get_segment(subpath1.len_segments() - 1).unwrap(); - let first_segment = subpath2.get_segment(0).unwrap(); + let last_segment_end = point_to_dvec2(bezpath1.segments().last().unwrap().end()); + let first_segment_start = point_to_dvec2(bezpath2.segments().next().unwrap().start()); // 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) { + 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 = subpath.get_segment(i).unwrap().tangent(TValue::Parametric(1.)); - let in_tangent = subpath.get_segment(j).unwrap().tangent(TValue::Parametric(0.)); - let angle = out_tangent.angle_to(in_tangent); - // The angle is concave. The Subpath overlap and must be clipped 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 - // 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_join = false; - } + + if let Some((clipped_subpath1, clipped_subpath2)) = clip_simple_bezpaths(bezpath1, bezpath2) { + bezpaths[i] = clipped_subpath1; + bezpaths[j] = clipped_subpath2; + apply_join = 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(miter_limit) => { - let miter_manipulator_group = subpaths[i].miter_line_join(&subpaths[j], miter_limit); - if let Some(miter_manipulator_group) = miter_manipulator_group { - subpaths[i].manipulator_groups_mut().push(miter_manipulator_group); + Join::Bevel => { + let element = PathEl::LineTo(bezpaths[j].segments().next().unwrap().start()); + bezpaths[i].push(element); + } + Join::Miter => { + let element = miter_line_join(&bezpaths[i], &bezpaths[j], miter_limit); + if let Some(element) = element { + bezpaths[i].push(element[0]); + bezpaths[i].push(element[1]); + } else { + let element = PathEl::LineTo(bezpaths[j].segments().next().unwrap().start()); + bezpaths[i].push(element); } } Join::Round => { - let (out_handle, round_point, in_handle) = subpaths[i].round_line_join(&subpaths[j], subpath.manipulator_groups()[j].anchor); - let last_index = subpaths[i].manipulator_groups().len() - 1; - subpaths[i].manipulator_groups_mut()[last_index].out_handle = Some(out_handle); - subpaths[i].manipulator_groups_mut().push(round_point); - subpaths[j].manipulator_groups_mut()[0].in_handle = Some(in_handle); + let center = point_to_dvec2(bezpath.get_seg(i + 1).unwrap().end()); + let elements = round_line_join(&bezpaths[i], &bezpaths[j], center); + bezpaths[i].push(elements[0]); + bezpaths[i].push(elements[1]); } } } } // Clip any overlap in the last segment - if subpath.closed { - let out_tangent = subpath.get_segment(subpath.len_segments() - 1).unwrap().tangent(TValue::Parametric(1.)); - let in_tangent = subpath.get_segment(0).unwrap().tangent(TValue::Parametric(0.)); - let angle = out_tangent.angle_to(in_tangent); - + let is_bezpath_closed = bezpath.elements().last().is_some_and(|element| *element == PathEl::ClosePath); + if is_bezpath_closed { 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_join = false; - } + if let Some((clipped_subpath1, clipped_subpath2)) = clip_simple_bezpaths(&bezpaths[bezpaths.len() - 1], &bezpaths[0]) { + // Merge the clipped subpaths + let last_index = bezpaths.len() - 1; + bezpaths[last_index] = clipped_subpath1; + bezpaths[0] = clipped_subpath2; + apply_join = false; } + if apply_join { - drop_common_point[0] = false; match join { - Join::Bevel => {} - Join::Miter(miter_limit) => { - let last_subpath_index = subpaths.len() - 1; - let miter_manipulator_group = subpaths[last_subpath_index].miter_line_join(&subpaths[0], miter_limit); - if let Some(miter_manipulator_group) = miter_manipulator_group { - subpaths[last_subpath_index].manipulator_groups_mut().push(miter_manipulator_group); + Join::Bevel => { + let last_subpath_index = bezpaths.len() - 1; + let element = PathEl::LineTo(bezpaths[0].segments().next().unwrap().start()); + bezpaths[last_subpath_index].push(element); + } + Join::Miter => { + let last_subpath_index = bezpaths.len() - 1; + let element = miter_line_join(&bezpaths[last_subpath_index], &bezpaths[0], miter_limit); + if let Some(element) = element { + bezpaths[last_subpath_index].push(element[0]); + bezpaths[last_subpath_index].push(element[1]); + } else { + let element = PathEl::LineTo(bezpaths[0].segments().next().unwrap().start()); + bezpaths[last_subpath_index].push(element); } } 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], subpath.manipulator_groups()[0].anchor); - let last_index = subpaths[last_subpath_index].manipulator_groups().len() - 1; - subpaths[last_subpath_index].manipulator_groups_mut()[last_index].out_handle = Some(out_handle); - subpaths[last_subpath_index].manipulator_groups_mut().push(round_point); - subpaths[0].manipulator_groups_mut()[0].in_handle = Some(in_handle); + let last_subpath_index = bezpaths.len() - 1; + let center = point_to_dvec2(bezpath.get_seg(1).unwrap().start()); + let elements = round_line_join(&bezpaths[last_subpath_index], &bezpaths[0], center); + bezpaths[last_subpath_index].push(elements[0]); + bezpaths[last_subpath_index].push(elements[1]); } } } } - // Merge the subpaths. Drop points which overlap with one another. - let mut manipulator_groups = subpaths[0].manipulator_groups().to_vec(); - 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().to_vec(); - manipulators_copy[0].in_handle = last_group.in_handle; - - manipulator_groups.append(&mut manipulators_copy); - } else { - manipulator_groups.append(&mut subpaths[i].manipulator_groups().to_vec()); + // Merge the bezpaths and its segments. Drop points which overlap with one another. + let segments = bezpaths.iter().flat_map(|bezpath| bezpath.segments().collect::>()).collect::>(); + let mut offset_bezpath = segments.iter().fold(BezPath::new(), |mut acc, segment| { + if acc.elements().is_empty() { + acc.move_to(segment.start()); } - } - if subpath.closed && drop_common_point[0] { - let last_group = manipulator_groups.pop().unwrap(); - manipulator_groups[0].in_handle = last_group.in_handle; + acc.push(segment.as_path_el()); + acc + }); + + if is_bezpath_closed { + offset_bezpath.close_path(); } - Subpath::new(manipulator_groups, subpath.closed) + offset_bezpath } diff --git a/node-graph/gcore/src/vector/algorithms/util.rs b/node-graph/gcore/src/vector/algorithms/util.rs new file mode 100644 index 0000000000..5f70428abf --- /dev/null +++ b/node-graph/gcore/src/vector/algorithms/util.rs @@ -0,0 +1,44 @@ +use glam::DVec2; +use kurbo::{ParamCurve, ParamCurveDeriv, PathSeg}; + +pub fn segment_tangent(segment: PathSeg, t: f64) -> DVec2 { + // NOTE: .deriv() method gives inaccurate result when it is 1. + let t = if t == 1. { 1. - f64::EPSILON } else { t }; + + let tangent = match segment { + PathSeg::Line(line) => line.deriv().eval(t), + PathSeg::Quad(quad_bez) => quad_bez.deriv().eval(t), + PathSeg::Cubic(cubic_bez) => cubic_bez.deriv().eval(t), + }; + + DVec2::new(tangent.x, tangent.y) +} + +// 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 { + (f1 - f2).abs() < super::contants::MAX_ABSOLUTE_DIFFERENCE +} + +/// Compare points by allowing some maximum absolute difference to account for floating point errors +#[cfg(test)] +pub fn compare_points(p1: kurbo::Point, p2: kurbo::Point) -> bool { + let (p1, p2) = (crate::vector::misc::point_to_dvec2(p1), crate::vector::misc::point_to_dvec2(p2)); + p1.abs_diff_eq(p2, super::contants::MAX_ABSOLUTE_DIFFERENCE) +} + +/// 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) + .map(|(p1, p2)| (crate::vector::misc::point_to_dvec2(p1), crate::vector::misc::point_to_dvec2(p2))) + .all(|(p1, p2)| p1.abs_diff_eq(p2, max_absolute_difference)) +} + +/// Compare the two values in a `DVec2` independently with a provided max absolute value difference. +#[cfg(test)] +pub fn dvec2_compare(a: kurbo::Point, b: kurbo::Point, max_abs_diff: f64) -> glam::BVec2 { + glam::BVec2::new((a.x - b.x).abs() < max_abs_diff, (a.y - b.y).abs() < max_abs_diff) +} diff --git a/node-graph/gcore/src/vector/vector_nodes.rs b/node-graph/gcore/src/vector/vector_nodes.rs index 235046d994..85d6ec503e 100644 --- a/node-graph/gcore/src/vector/vector_nodes.rs +++ b/node-graph/gcore/src/vector/vector_nodes.rs @@ -1,5 +1,5 @@ -use super::algorithms::bezpath_algorithms::{self, position_on_bezpath, sample_polyline_on_bezpath, split_bezpath, tangent_on_bezpath}; -use super::algorithms::offset_subpath::offset_subpath; +use super::algorithms::bezpath_algorithms::{self, evaluate_bezpath, sample_polyline_on_bezpath, split_bezpath, tangent_on_bezpath}; +use super::algorithms::offset_subpath::offset_bezpath; use super::algorithms::spline::{solve_spline_first_handle_closed, solve_spline_first_handle_open}; use super::misc::{CentroidType, point_to_dvec2}; use super::style::{Fill, Gradient, GradientStops, Stroke}; @@ -17,8 +17,7 @@ use crate::vector::misc::{handles_to_segment, segment_to_handles}; use crate::vector::style::{PaintOrder, StrokeAlign, StrokeCap, StrokeJoin}; use crate::vector::{FillId, RegionId}; use crate::{CloneVarArgs, Color, Context, Ctx, ExtractAll, GraphicElement, GraphicGroupTable, OwnedContextImpl}; - -use bezier_rs::{BezierHandles, Join, ManipulatorGroup, Subpath}; +use bezier_rs::{BezierHandles, ManipulatorGroup, Subpath}; use core::f64::consts::PI; use core::hash::{Hash, Hasher}; use glam::{DAffine2, DVec2}; @@ -979,10 +978,10 @@ async fn offset_path(_: impl Ctx, vector_data: VectorDataTable, distance: f64, j vector_data .instance_iter() .map(|mut vector_data_instance| { - let vector_data_transform = vector_data_instance.transform; + let vector_data_transform = Affine::new(vector_data_instance.transform.to_cols_array()); let vector_data = vector_data_instance.instance; - let subpaths = vector_data.stroke_bezier_paths(); + let bezpaths = vector_data.stroke_bezpath_iter(); let mut result = VectorData { style: vector_data.style.clone(), ..Default::default() @@ -990,24 +989,25 @@ async fn offset_path(_: impl Ctx, vector_data: VectorDataTable, distance: f64, j result.style.set_stroke_transform(DAffine2::IDENTITY); // Perform operation on all subpaths in this shape. - for mut subpath in subpaths { - subpath.apply_transform(vector_data_transform); + for mut bezpath in bezpaths { + bezpath.apply_affine(vector_data_transform); // Taking the existing stroke data and passing it to Bezier-rs to generate new paths. - let mut subpath_out = offset_subpath( - &subpath, + let mut bezpath_out = offset_bezpath( + &bezpath, -distance, match join { - StrokeJoin::Miter => Join::Miter(Some(miter_limit)), - StrokeJoin::Bevel => Join::Bevel, - StrokeJoin::Round => Join::Round, + StrokeJoin::Miter => kurbo::Join::Miter, + StrokeJoin::Bevel => kurbo::Join::Bevel, + StrokeJoin::Round => kurbo::Join::Round, }, + Some(miter_limit), ); - subpath_out.apply_transform(vector_data_transform.inverse()); + bezpath_out.apply_affine(vector_data_transform.inverse()); // One closed subpath, open path. - result.append_subpath(subpath_out, false); + result.append_bezpath(bezpath_out); } vector_data_instance.instance = result; @@ -1325,7 +1325,7 @@ async fn position_on_path( let t = if progress == bezpath_count { 1. } else { progress.fract() }; bezpath.apply_affine(Affine::new(transform.to_cols_array())); - point_to_dvec2(position_on_bezpath(bezpath, t, euclidian, None)) + point_to_dvec2(evaluate_bezpath(bezpath, t, euclidian, None)) }) }