From 9831440ca945f60235c3c0f4bca6364fdc5e5e85 Mon Sep 17 00:00:00 2001 From: indierusty Date: Tue, 22 Jul 2025 11:41:01 +0530 Subject: [PATCH 01/17] impl function for segment intersections --- .../src/vector/algorithms/intersection.rs | 39 +++++++++++++++++++ node-graph/gcore/src/vector/algorithms/mod.rs | 1 + 2 files changed, 40 insertions(+) create mode 100644 node-graph/gcore/src/vector/algorithms/intersection.rs 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..a97f1a7bd5 --- /dev/null +++ b/node-graph/gcore/src/vector/algorithms/intersection.rs @@ -0,0 +1,39 @@ +use kurbo::{BezPath, ParamCurve, PathSeg, Shape}; + +pub fn segment_intersections(segment1: PathSeg, segment2: PathSeg, accuracy: f64) -> Vec<(f64, f64)> { + let mut intersections = Vec::new(); + segment_intersections_inner(segment1, 0., 1., segment2, 0., 1., accuracy, &mut intersections); + intersections +} + +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 are within the error threshold (i.e. are small enough), we have found an intersection + if bbox1.width() < accuracy && bbox1.height() < accuracy { + // Use the middle t value, append the corresponding `t` value. + intersections.push((mid_t1, mid_t2)); + return; + } + + // Split curves in half and repeat with the combinations of the two halves of each curve + let (seg11, seg12) = segment1.subdivide(); + let (seg21, seg22) = segment2.subdivide(); + + 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); + } +} + +fn bezpath_intersections(bezpath1: &BezPath, bezpath2: &BezPath) -> Vec { + let intersections = Vec::new(); + intersections +} diff --git a/node-graph/gcore/src/vector/algorithms/mod.rs b/node-graph/gcore/src/vector/algorithms/mod.rs index ed44f25590..639f68225c 100644 --- a/node-graph/gcore/src/vector/algorithms/mod.rs +++ b/node-graph/gcore/src/vector/algorithms/mod.rs @@ -1,5 +1,6 @@ pub mod bezpath_algorithms; pub mod instance; +pub mod intersection; pub mod merge_by_distance; pub mod offset_subpath; pub mod poisson_disk; From 9070ab9ab677b15923a144ef26f0a93237594106 Mon Sep 17 00:00:00 2001 From: indierusty Date: Tue, 22 Jul 2025 11:47:57 +0530 Subject: [PATCH 02/17] fix and improve segment intersections --- .../gcore/src/vector/algorithms/intersection.rs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/node-graph/gcore/src/vector/algorithms/intersection.rs b/node-graph/gcore/src/vector/algorithms/intersection.rs index a97f1a7bd5..6465eee46e 100644 --- a/node-graph/gcore/src/vector/algorithms/intersection.rs +++ b/node-graph/gcore/src/vector/algorithms/intersection.rs @@ -1,9 +1,15 @@ use kurbo::{BezPath, ParamCurve, PathSeg, Shape}; pub fn segment_intersections(segment1: PathSeg, segment2: PathSeg, accuracy: f64) -> Vec<(f64, f64)> { - let mut intersections = Vec::new(); - segment_intersections_inner(segment1, 0., 1., segment2, 0., 1., accuracy, &mut intersections); - intersections + 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 + } + } } 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)>) { @@ -16,7 +22,7 @@ fn segment_intersections_inner(segment1: PathSeg, min_t1: f64, max_t1: f64, segm // Check if the bounding boxes overlap if bbox1.overlaps(bbox2) { // If bounding boxes are within the error threshold (i.e. are small enough), we have found an intersection - if bbox1.width() < accuracy && bbox1.height() < accuracy { + 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; From 1bfed808d503bdd2037ade285acb81f099379f55 Mon Sep 17 00:00:00 2001 From: indierusty Date: Tue, 22 Jul 2025 12:57:46 +0530 Subject: [PATCH 03/17] copy and refactor related segment intersection methods --- .../src/vector/algorithms/intersection.rs | 58 ++++++++++++++++++- 1 file changed, 56 insertions(+), 2 deletions(-) diff --git a/node-graph/gcore/src/vector/algorithms/intersection.rs b/node-graph/gcore/src/vector/algorithms/intersection.rs index 6465eee46e..e3aaa657db 100644 --- a/node-graph/gcore/src/vector/algorithms/intersection.rs +++ b/node-graph/gcore/src/vector/algorithms/intersection.rs @@ -1,6 +1,11 @@ -use kurbo::{BezPath, ParamCurve, PathSeg, Shape}; +use kurbo::{BezPath, DEFAULT_ACCURACY, ParamCurve, PathSeg, Shape}; + +/// Minimum allowable separation between adjacent `t` values when calculating curve intersections +pub const MIN_SEPARATION_VALUE: f64 = 5. * 1e-3; + +pub fn segment_intersections(segment1: PathSeg, segment2: PathSeg, accuracy: Option) -> Vec<(f64, f64)> { + let accuracy = accuracy.unwrap_or(DEFAULT_ACCURACY); -pub fn segment_intersections(segment1: PathSeg, segment2: PathSeg, accuracy: f64) -> Vec<(f64, f64)> { 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(), @@ -39,6 +44,55 @@ fn segment_intersections_inner(segment1: PathSeg, min_t1: f64, max_t1: f64, segm } } +// 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 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. +/// - `minimum_separation` - 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 { + // TODO: Consider using the `intersections_between_vectors_of_curves` helper function here + // Otherwise, use bounding box to determine intersections + 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 2 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, `error` defines the threshold for bounding boxes to be considered an intersection point. +/// - `minimum_separation` - 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)> { + // TODO: Consider using the `intersections_between_vectors_of_curves` helper function here + // Otherwise, use bounding box to determine intersections + 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 + }) +} + fn bezpath_intersections(bezpath1: &BezPath, bezpath2: &BezPath) -> Vec { let intersections = Vec::new(); intersections From e6b114afa90d779d2cb05bc7120ecfacbcbe40b9 Mon Sep 17 00:00:00 2001 From: indierusty Date: Tue, 22 Jul 2025 13:50:11 +0530 Subject: [PATCH 04/17] copy and refactor tests for segment intersection from bezier-rs --- .../gcore/src/vector/algorithms/contants.rs | 5 ++ .../src/vector/algorithms/intersection.rs | 69 ++++++++++++++++++- node-graph/gcore/src/vector/algorithms/mod.rs | 2 + .../gcore/src/vector/algorithms/util.rs | 26 +++++++ 4 files changed, 100 insertions(+), 2 deletions(-) create mode 100644 node-graph/gcore/src/vector/algorithms/contants.rs create mode 100644 node-graph/gcore/src/vector/algorithms/util.rs 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..c970059fa7 --- /dev/null +++ b/node-graph/gcore/src/vector/algorithms/contants.rs @@ -0,0 +1,5 @@ +/// 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. +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 index e3aaa657db..ced452cdb7 100644 --- a/node-graph/gcore/src/vector/algorithms/intersection.rs +++ b/node-graph/gcore/src/vector/algorithms/intersection.rs @@ -1,7 +1,6 @@ use kurbo::{BezPath, DEFAULT_ACCURACY, ParamCurve, PathSeg, Shape}; -/// Minimum allowable separation between adjacent `t` values when calculating curve intersections -pub const MIN_SEPARATION_VALUE: f64 = 5. * 1e-3; +use super::contants::MIN_SEPARATION_VALUE; pub fn segment_intersections(segment1: PathSeg, segment2: PathSeg, accuracy: Option) -> Vec<(f64, f64)> { let accuracy = accuracy.unwrap_or(DEFAULT_ACCURACY); @@ -97,3 +96,69 @@ fn bezpath_intersections(bezpath1: &BezPath, bezpath2: &BezPath) -> Vec { let intersections = Vec::new(); intersections } + +#[cfg(test)] +mod tests { + use super::filtered_segment_intersections; + use crate::vector::algorithms::util::{compare_points, compare_vec_of_points}; + + use kurbo::{CubicBez, Line, ParamCurve, 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 bezier = PathSeg::Cubic(CubicBez::new(p1, p2, p3, p4)); + + let line = PathSeg::Line(Line::new(Point::new(150., 150.), Point::new(20., 20.))); + let intersections = filtered_segment_intersections(bezier, line, 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 bezier1 = 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 bezier2 = PathSeg::Quad(QuadBez::new(p0, p1, p2)); + + let intersections1 = filtered_segment_intersections(bezier1, bezier2, None, None); + let intersections2 = filtered_segment_intersections(bezier2, bezier1, None, None); + + let intersections1_points: Vec = intersections1.iter().map(|&t| bezier1.eval(t)).collect(); + let intersections2_points: Vec = intersections2.iter().map(|&t| bezier2.eval(t)).rev().collect(); + + assert!(compare_vec_of_points(intersections1_points, intersections2_points, 2.)); + } +} diff --git a/node-graph/gcore/src/vector/algorithms/mod.rs b/node-graph/gcore/src/vector/algorithms/mod.rs index 639f68225c..b9284f327d 100644 --- a/node-graph/gcore/src/vector/algorithms/mod.rs +++ b/node-graph/gcore/src/vector/algorithms/mod.rs @@ -1,7 +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/util.rs b/node-graph/gcore/src/vector/algorithms/util.rs new file mode 100644 index 0000000000..7fd71ba378 --- /dev/null +++ b/node-graph/gcore/src/vector/algorithms/util.rs @@ -0,0 +1,26 @@ +use super::contants::MAX_ABSOLUTE_DIFFERENCE; +use crate::vector::misc::point_to_dvec2; + +use kurbo::Point; + +// 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() < MAX_ABSOLUTE_DIFFERENCE +} + +/// Compare points by allowing some maximum absolute difference to account for floating point errors +pub fn compare_points(p1: Point, p2: Point) -> bool { + let (p1, p2) = (point_to_dvec2(p1), point_to_dvec2(p2)); + p1.abs_diff_eq(p2, 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)| (point_to_dvec2(p1), point_to_dvec2(p2))) + .all(|(p1, p2)| p1.abs_diff_eq(p2, max_absolute_difference)) +} From eb3972caa255157085bf2c9ef12154f2b58eb6bd Mon Sep 17 00:00:00 2001 From: indierusty Date: Wed, 23 Jul 2025 12:54:20 +0530 Subject: [PATCH 05/17] impl intersection with bezpaths --- .../src/vector/algorithms/intersection.rs | 33 ++++++++++++++++--- .../gcore/src/vector/algorithms/util.rs | 6 ++++ 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/node-graph/gcore/src/vector/algorithms/intersection.rs b/node-graph/gcore/src/vector/algorithms/intersection.rs index ced452cdb7..2796a35079 100644 --- a/node-graph/gcore/src/vector/algorithms/intersection.rs +++ b/node-graph/gcore/src/vector/algorithms/intersection.rs @@ -2,6 +2,34 @@ use kurbo::{BezPath, DEFAULT_ACCURACY, ParamCurve, PathSeg, Shape}; use super::contants::MIN_SEPARATION_VALUE; +/// 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 occurred. +/// - `minimum_separation`: the minimum difference 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 +} + pub fn segment_intersections(segment1: PathSeg, segment2: PathSeg, accuracy: Option) -> Vec<(f64, f64)> { let accuracy = accuracy.unwrap_or(DEFAULT_ACCURACY); @@ -92,11 +120,6 @@ pub fn filtered_all_segment_intersections(segment1: PathSeg, segment2: PathSeg, }) } -fn bezpath_intersections(bezpath1: &BezPath, bezpath2: &BezPath) -> Vec { - let intersections = Vec::new(); - intersections -} - #[cfg(test)] mod tests { use super::filtered_segment_intersections; diff --git a/node-graph/gcore/src/vector/algorithms/util.rs b/node-graph/gcore/src/vector/algorithms/util.rs index 7fd71ba378..4c396f9ad7 100644 --- a/node-graph/gcore/src/vector/algorithms/util.rs +++ b/node-graph/gcore/src/vector/algorithms/util.rs @@ -1,6 +1,7 @@ use super::contants::MAX_ABSOLUTE_DIFFERENCE; use crate::vector::misc::point_to_dvec2; +use glam::BVec2; use kurbo::Point; // Compare two f64s with some maximum absolute difference to account for floating point errors @@ -24,3 +25,8 @@ pub fn compare_vec_of_points(a: Vec, b: Vec, max_absolute_differen .map(|(p1, p2)| (point_to_dvec2(p1), 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. +pub fn dvec2_compare(a: Point, b: Point, max_abs_diff: f64) -> BVec2 { + BVec2::new((a.x - b.x).abs() < max_abs_diff, (a.y - b.y).abs() < max_abs_diff) +} From 30a79514df10e4d0fd093ae027d000f285896d2b Mon Sep 17 00:00:00 2001 From: indierusty Date: Wed, 23 Jul 2025 12:56:45 +0530 Subject: [PATCH 06/17] copy and refactor tests --- .../src/vector/algorithms/intersection.rs | 178 +++++++++++++++++- 1 file changed, 175 insertions(+), 3 deletions(-) diff --git a/node-graph/gcore/src/vector/algorithms/intersection.rs b/node-graph/gcore/src/vector/algorithms/intersection.rs index 2796a35079..0299f2f85a 100644 --- a/node-graph/gcore/src/vector/algorithms/intersection.rs +++ b/node-graph/gcore/src/vector/algorithms/intersection.rs @@ -122,10 +122,13 @@ pub fn filtered_all_segment_intersections(segment1: PathSeg, segment2: PathSeg, #[cfg(test)] mod tests { - use super::filtered_segment_intersections; - use crate::vector::algorithms::util::{compare_points, compare_vec_of_points}; + 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::{CubicBez, Line, ParamCurve, PathSeg, Point, QuadBez}; + use kurbo::{BezPath, CubicBez, Line, ParamCurve, PathEl, PathSeg, Point, QuadBez}; #[test] fn test_intersect_line_segment_quadratic() { @@ -184,4 +187,173 @@ mod tests { 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_bezier = PathSeg::Cubic(CubicBez::new(cubic_start, cubic_handle_1, cubic_handle_2, cubic_end)); + let quadratic_bezier_1 = 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_bezier, line, None, None); + let quadratic_1_intersections = filtered_segment_intersections(quadratic_bezier_1, line, None, None); + let subpath_intersections = bezpath_and_segment_intersections(&bezpath, line, None, None); + + assert!( + dvec2_compare( + cubic_bezier.eval(cubic_intersections[0]), + bezpath.segments().nth(subpath_intersections[0].0).unwrap().eval(subpath_intersections[0].1), + MAX_ABSOLUTE_DIFFERENCE + ) + .all() + ); + + assert!( + dvec2_compare( + quadratic_bezier_1.eval(quadratic_1_intersections[0]), + bezpath.segments().nth(subpath_intersections[1].0).unwrap().eval(subpath_intersections[1].1), + MAX_ABSOLUTE_DIFFERENCE + ) + .all() + ); + + assert!( + dvec2_compare( + quadratic_bezier_1.eval(quadratic_1_intersections[1]), + bezpath.segments().nth(subpath_intersections[2].0).unwrap().eval(subpath_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_bezier = PathSeg::Cubic(CubicBez::new(cubic_start, cubic_handle_1, cubic_handle_2, cubic_end)); + let quadratic_bezier_1 = 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_bezier, line, None, None); + let quadratic_1_intersections = filtered_segment_intersections(quadratic_bezier_1, line, None, None); + let subpath_intersections = bezpath_and_segment_intersections(&bezpath, line, None, None); + + assert!( + dvec2_compare( + cubic_bezier.eval(cubic_intersections[0]), + bezpath.segments().nth(subpath_intersections[0].0).unwrap().eval(subpath_intersections[0].1), + MAX_ABSOLUTE_DIFFERENCE + ) + .all() + ); + + assert!( + dvec2_compare( + quadratic_bezier_1.eval(quadratic_1_intersections[0]), + bezpath.segments().nth(subpath_intersections[1].0).unwrap().eval(subpath_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_bezier = PathSeg::Cubic(CubicBez::new(cubic_start, cubic_handle_1, cubic_handle_2, cubic_end)); + let quadratic_bezier_1 = 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_bezier, line, None, None); + let quadratic_1_intersections = filtered_segment_intersections(quadratic_bezier_1, line, None, None); + let subpath_intersections = bezpath_and_segment_intersections(&bezpath, line, None, None); + + assert!( + dvec2_compare( + cubic_bezier.eval(cubic_intersections[0]), + bezpath.segments().nth(subpath_intersections[0].0).unwrap().eval(subpath_intersections[0].1), + MAX_ABSOLUTE_DIFFERENCE + ) + .all() + ); + + assert!( + dvec2_compare( + quadratic_bezier_1.eval(quadratic_1_intersections[0]), + bezpath.segments().nth(subpath_intersections[1].0).unwrap().eval(subpath_intersections[1].1), + MAX_ABSOLUTE_DIFFERENCE + ) + .all() + ); + + assert!( + dvec2_compare( + quadratic_bezier_1.eval(quadratic_1_intersections[1]), + bezpath.segments().nth(subpath_intersections[2].0).unwrap().eval(subpath_intersections[2].1), + MAX_ABSOLUTE_DIFFERENCE + ) + .all() + ); + } } From 936789e3fae35b93d7049a7f31ed11158f223344 Mon Sep 17 00:00:00 2001 From: indierusty Date: Wed, 23 Jul 2025 13:05:31 +0530 Subject: [PATCH 07/17] rename few variables in the tests module --- .../src/vector/algorithms/intersection.rs | 83 ++++++++++--------- 1 file changed, 42 insertions(+), 41 deletions(-) diff --git a/node-graph/gcore/src/vector/algorithms/intersection.rs b/node-graph/gcore/src/vector/algorithms/intersection.rs index 0299f2f85a..14fbf65f6e 100644 --- a/node-graph/gcore/src/vector/algorithms/intersection.rs +++ b/node-graph/gcore/src/vector/algorithms/intersection.rs @@ -148,6 +148,7 @@ mod tests { 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 @@ -156,10 +157,10 @@ mod tests { let p2 = Point::new(40., 40.); let p3 = Point::new(120., 120.); let p4 = Point::new(102., 29.); - let bezier = PathSeg::Cubic(CubicBez::new(p1, p2, p3, p4)); + let cubic_segment = PathSeg::Cubic(CubicBez::new(p1, p2, p3, p4)); - let line = PathSeg::Line(Line::new(Point::new(150., 150.), Point::new(20., 20.))); - let intersections = filtered_segment_intersections(bezier, line, None, None); + 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); } @@ -171,19 +172,19 @@ mod tests { let p2 = Point::new(150., 30.); let p3 = Point::new(160., 160.); - let bezier1 = PathSeg::Cubic(CubicBez::new(p0, p1, p2, p3)); + 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 bezier2 = PathSeg::Quad(QuadBez::new(p0, p1, p2)); + let quadratic_segment = PathSeg::Quad(QuadBez::new(p0, p1, p2)); - let intersections1 = filtered_segment_intersections(bezier1, bezier2, None, None); - let intersections2 = filtered_segment_intersections(bezier2, bezier1, None, None); + 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| bezier1.eval(t)).collect(); - let intersections2_points: Vec = intersections2.iter().map(|&t| bezier2.eval(t)).rev().collect(); + 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.)); } @@ -202,8 +203,8 @@ mod tests { let quadratic_2_handle = Point::new(70., 185.); - let cubic_bezier = PathSeg::Cubic(CubicBez::new(cubic_start, cubic_handle_1, cubic_handle_2, cubic_end)); - let quadratic_bezier_1 = PathSeg::Quad(QuadBez::new(cubic_end, quadratic_1_handle, quadratic_end)); + 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), @@ -213,16 +214,16 @@ mod tests { PathEl::ClosePath, ]); - let line = PathSeg::Line(Line::new(Point::new(150., 150.), Point::new(20., 20.))); + let linear_segment = PathSeg::Line(Line::new(Point::new(150., 150.), Point::new(20., 20.))); - let cubic_intersections = filtered_segment_intersections(cubic_bezier, line, None, None); - let quadratic_1_intersections = filtered_segment_intersections(quadratic_bezier_1, line, None, None); - let subpath_intersections = bezpath_and_segment_intersections(&bezpath, line, None, None); + 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_bezier.eval(cubic_intersections[0]), - bezpath.segments().nth(subpath_intersections[0].0).unwrap().eval(subpath_intersections[0].1), + cubic_segment.eval(cubic_intersections[0]), + bezpath.segments().nth(bezpath_intersections[0].0).unwrap().eval(bezpath_intersections[0].1), MAX_ABSOLUTE_DIFFERENCE ) .all() @@ -230,8 +231,8 @@ mod tests { assert!( dvec2_compare( - quadratic_bezier_1.eval(quadratic_1_intersections[0]), - bezpath.segments().nth(subpath_intersections[1].0).unwrap().eval(subpath_intersections[1].1), + quadratic_segment.eval(quadratic_1_intersections[0]), + bezpath.segments().nth(bezpath_intersections[1].0).unwrap().eval(bezpath_intersections[1].1), MAX_ABSOLUTE_DIFFERENCE ) .all() @@ -239,8 +240,8 @@ mod tests { assert!( dvec2_compare( - quadratic_bezier_1.eval(quadratic_1_intersections[1]), - bezpath.segments().nth(subpath_intersections[2].0).unwrap().eval(subpath_intersections[2].1), + quadratic_segment.eval(quadratic_1_intersections[1]), + bezpath.segments().nth(bezpath_intersections[2].0).unwrap().eval(bezpath_intersections[2].1), MAX_ABSOLUTE_DIFFERENCE ) .all() @@ -262,8 +263,8 @@ mod tests { let quadratic_2_handle = Point::new(70., 185.); - let cubic_bezier = PathSeg::Cubic(CubicBez::new(cubic_start, cubic_handle_1, cubic_handle_2, cubic_end)); - let quadratic_bezier_1 = PathSeg::Quad(QuadBez::new(cubic_end, quadratic_1_handle, quadratic_end)); + 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), @@ -275,14 +276,14 @@ mod tests { let line = PathSeg::Line(Line::new(Point::new(150., 150.), Point::new(20., 20.))); - let cubic_intersections = filtered_segment_intersections(cubic_bezier, line, None, None); - let quadratic_1_intersections = filtered_segment_intersections(quadratic_bezier_1, line, None, None); - let subpath_intersections = bezpath_and_segment_intersections(&bezpath, line, None, None); + 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_bezier.eval(cubic_intersections[0]), - bezpath.segments().nth(subpath_intersections[0].0).unwrap().eval(subpath_intersections[0].1), + cubic_segment.eval(cubic_intersections[0]), + bezpath.segments().nth(bezpath_intersections[0].0).unwrap().eval(bezpath_intersections[0].1), MAX_ABSOLUTE_DIFFERENCE ) .all() @@ -290,8 +291,8 @@ mod tests { assert!( dvec2_compare( - quadratic_bezier_1.eval(quadratic_1_intersections[0]), - bezpath.segments().nth(subpath_intersections[1].0).unwrap().eval(subpath_intersections[1].1), + quadratic_segment.eval(quadratic_1_intersections[0]), + bezpath.segments().nth(bezpath_intersections[1].0).unwrap().eval(bezpath_intersections[1].1), MAX_ABSOLUTE_DIFFERENCE ) .all() @@ -312,8 +313,8 @@ mod tests { let quadratic_2_handle = Point::new(70., 185.); - let cubic_bezier = PathSeg::Cubic(CubicBez::new(cubic_start, cubic_handle_1, cubic_handle_2, cubic_end)); - let quadratic_bezier_1 = PathSeg::Quad(QuadBez::new(cubic_end, quadratic_1_handle, quadratic_end)); + 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), @@ -325,14 +326,14 @@ mod tests { let line = PathSeg::Line(Line::new(Point::new(150., 150.), Point::new(20., 20.))); - let cubic_intersections = filtered_segment_intersections(cubic_bezier, line, None, None); - let quadratic_1_intersections = filtered_segment_intersections(quadratic_bezier_1, line, None, None); - let subpath_intersections = bezpath_and_segment_intersections(&bezpath, line, None, None); + 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_bezier.eval(cubic_intersections[0]), - bezpath.segments().nth(subpath_intersections[0].0).unwrap().eval(subpath_intersections[0].1), + cubic_segment.eval(cubic_intersections[0]), + bezpath.segments().nth(bezpath_intersections[0].0).unwrap().eval(bezpath_intersections[0].1), MAX_ABSOLUTE_DIFFERENCE ) .all() @@ -340,8 +341,8 @@ mod tests { assert!( dvec2_compare( - quadratic_bezier_1.eval(quadratic_1_intersections[0]), - bezpath.segments().nth(subpath_intersections[1].0).unwrap().eval(subpath_intersections[1].1), + quadratic_segment.eval(quadratic_1_intersections[0]), + bezpath.segments().nth(bezpath_intersections[1].0).unwrap().eval(bezpath_intersections[1].1), MAX_ABSOLUTE_DIFFERENCE ) .all() @@ -349,8 +350,8 @@ mod tests { assert!( dvec2_compare( - quadratic_bezier_1.eval(quadratic_1_intersections[1]), - bezpath.segments().nth(subpath_intersections[2].0).unwrap().eval(subpath_intersections[2].1), + quadratic_segment.eval(quadratic_1_intersections[1]), + bezpath.segments().nth(bezpath_intersections[2].0).unwrap().eval(bezpath_intersections[2].1), MAX_ABSOLUTE_DIFFERENCE ) .all() From 7b43ea199e4299613e7dfd73b93ead58ab6d35cc Mon Sep 17 00:00:00 2001 From: indierusty Date: Wed, 23 Jul 2025 13:36:44 +0530 Subject: [PATCH 08/17] rename position_on_bezpath to evaluate_bezpath --- node-graph/gcore/src/vector/algorithms/bezpath_algorithms.rs | 2 +- node-graph/gcore/src/vector/vector_nodes.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/node-graph/gcore/src/vector/algorithms/bezpath_algorithms.rs b/node-graph/gcore/src/vector/algorithms/bezpath_algorithms.rs index 109ef31e8a..f510a8185f 100644 --- a/node-graph/gcore/src/vector/algorithms/bezpath_algorithms.rs +++ b/node-graph/gcore/src/vector/algorithms/bezpath_algorithms.rs @@ -53,7 +53,7 @@ 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 { +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) } diff --git a/node-graph/gcore/src/vector/vector_nodes.rs b/node-graph/gcore/src/vector/vector_nodes.rs index 93e732938b..ca07c37cb8 100644 --- a/node-graph/gcore/src/vector/vector_nodes.rs +++ b/node-graph/gcore/src/vector/vector_nodes.rs @@ -1,4 +1,4 @@ -use super::algorithms::bezpath_algorithms::{self, position_on_bezpath, sample_polyline_on_bezpath, split_bezpath, tangent_on_bezpath}; +use super::algorithms::bezpath_algorithms::{self, evaluate_bezpath, sample_polyline_on_bezpath, split_bezpath, tangent_on_bezpath}; use super::algorithms::offset_subpath::offset_subpath; use super::algorithms::spline::{solve_spline_first_handle_closed, solve_spline_first_handle_open}; use super::misc::{CentroidType, point_to_dvec2}; @@ -1342,7 +1342,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)) }) } From a3ea7ff8752ae1def305063ac36e80a99f0ec9a9 Mon Sep 17 00:00:00 2001 From: indierusty Date: Wed, 23 Jul 2025 13:46:35 +0530 Subject: [PATCH 09/17] copy and refactor function to clip two intersecting simple bezpaths --- .../vector/algorithms/bezpath_algorithms.rs | 42 +++++++++++++++++-- 1 file changed, 39 insertions(+), 3 deletions(-) diff --git a/node-graph/gcore/src/vector/algorithms/bezpath_algorithms.rs b/node-graph/gcore/src/vector/algorithms/bezpath_algorithms.rs index f510a8185f..d493906585 100644 --- a/node-graph/gcore/src/vector/algorithms/bezpath_algorithms.rs +++ b/node-graph/gcore/src/vector/algorithms/bezpath_algorithms.rs @@ -1,18 +1,18 @@ +use super::intersection::bezpath_intersections; use super::poisson_disk::poisson_disk_sample; use crate::vector::algorithms::offset_subpath::MAX_ABSOLUTE_DIFFERENCE; use crate::vector::misc::{PointSpacingType, dvec2_to_point}; use glam::DVec2; use kurbo::{BezPath, CubicBez, DEFAULT_ACCURACY, Line, ParamCurve, ParamCurveDeriv, PathEl, PathSeg, Point, QuadBez, Rect, Shape}; -/// 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,6 +53,18 @@ pub fn split_bezpath(bezpath: &BezPath, t: f64, euclidian: bool) -> Option<(BezP Some((first_bezpath, second_bezpath)) } +/// Splits the [`BezPath`] 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)> { + 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 +340,27 @@ 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 +// 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. +pub fn clip_simple_bezpaths(bezpath1: &BezPath, bezpath2: &BezPath) -> Option<(BezPath, BezPath)> { + // Split the first subpath at its last intersection + let intersections1 = bezpath_intersections(bezpath1, bezpath2, None, None); + if intersections1.is_empty() { + return None; + } + let (segment_index, t) = *intersections1.last().unwrap(); + let (clipped_subpath1, _) = split_bezpath_at_segment(bezpath1, segment_index, t)?; + + // Split the second subpath at its first intersection + let intersections2 = bezpath_intersections(bezpath2, bezpath1, None, None); + if intersections2.is_empty() { + return None; + } + let (segment_index, t) = intersections2[0]; + let (_, clipped_subpath2) = split_bezpath_at_segment(bezpath2, segment_index, t)?; + + Some((clipped_subpath1, clipped_subpath2)) +} From 880673f7254176423574623e25633ea4130c019c Mon Sep 17 00:00:00 2001 From: indierusty Date: Wed, 23 Jul 2025 13:49:36 +0530 Subject: [PATCH 10/17] refactor comments --- node-graph/gcore/src/vector/algorithms/bezpath_algorithms.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/node-graph/gcore/src/vector/algorithms/bezpath_algorithms.rs b/node-graph/gcore/src/vector/algorithms/bezpath_algorithms.rs index d493906585..21299fbdfd 100644 --- a/node-graph/gcore/src/vector/algorithms/bezpath_algorithms.rs +++ b/node-graph/gcore/src/vector/algorithms/bezpath_algorithms.rs @@ -343,8 +343,8 @@ pub fn is_linear(segment: &PathSeg) -> bool { // 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. +/// Helper function to clip overlap of two intersecting open Bezpaths. Returns an optional, as 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 intersections1 = bezpath_intersections(bezpath1, bezpath2, None, None); From 0869527cb53568292b29772104d52246cdb7e26d Mon Sep 17 00:00:00 2001 From: indierusty Date: Wed, 23 Jul 2025 15:19:12 +0530 Subject: [PATCH 11/17] copy and refactor functions for milter join --- .../vector/algorithms/bezpath_algorithms.rs | 55 ++++++++++++++++++- .../src/vector/algorithms/intersection.rs | 32 ++++++++++- .../gcore/src/vector/algorithms/util.rs | 19 ++++++- 3 files changed, 101 insertions(+), 5 deletions(-) diff --git a/node-graph/gcore/src/vector/algorithms/bezpath_algorithms.rs b/node-graph/gcore/src/vector/algorithms/bezpath_algorithms.rs index 21299fbdfd..00ce32ecc5 100644 --- a/node-graph/gcore/src/vector/algorithms/bezpath_algorithms.rs +++ b/node-graph/gcore/src/vector/algorithms/bezpath_algorithms.rs @@ -1,7 +1,8 @@ -use super::intersection::bezpath_intersections; +use super::intersection::{bezpath_intersections, line_intersection}; 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 crate::vector::misc::{PointSpacingType, dvec2_to_point, point_to_dvec2}; use glam::DVec2; use kurbo::{BezPath, CubicBez, DEFAULT_ACCURACY, Line, ParamCurve, ParamCurveDeriv, PathEl, PathSeg, Point, QuadBez, Rect, Shape}; @@ -364,3 +365,53 @@ pub fn clip_simple_bezpaths(bezpath1: &BezPath, bezpath2: &BezPath) -> Option<(B Some((clipped_subpath1, clipped_subpath2)) } + +/// Returns the manipulator point 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 manipulator group 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 { + 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.get_seg(0)?; + + 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 normalized_in_tangent = in_tangent.normalize(); + let normalized_out_tangent = out_tangent.normalize(); + + // The tangents must not be parallel for the miter join + if !normalized_in_tangent.abs_diff_eq(normalized_out_tangent, MAX_ABSOLUTE_DIFFERENCE) && !normalized_in_tangent.abs_diff_eq(-normalized_out_tangent, MAX_ABSOLUTE_DIFFERENCE) { + let intersection = line_intersection(in_segment.end(), in_tangent, out_segment.start(), out_tangent); + + let start_to_intersection = intersection - point_to_dvec2(in_segment.end()); + let intersection_to_end = point_to_dvec2(out_segment.start()) - intersection; + if start_to_intersection == DVec2::ZERO || intersection_to_end == DVec2::ZERO { + // Avoid panic from normalizing zero vectors + // TODO: Besides returning None, is there a more appropriate way to handle this? + return None; + } + + // Draw the miter join if the intersection occurs in the correct direction with respect to the path + if start_to_intersection.normalize().abs_diff_eq(in_tangent, MAX_ABSOLUTE_DIFFERENCE) + && intersection_to_end.normalize().abs_diff_eq(out_tangent, MAX_ABSOLUTE_DIFFERENCE) + && miter_limit > f64::EPSILON / (start_to_intersection.angle_to(-intersection_to_end).abs() / 2.).sin() + { + return Some(PathEl::LineTo(dvec2_to_point(intersection))); + } + } + // If we can't draw the miter join, default to a bevel join + None +} diff --git a/node-graph/gcore/src/vector/algorithms/intersection.rs b/node-graph/gcore/src/vector/algorithms/intersection.rs index 14fbf65f6e..c882f3107d 100644 --- a/node-graph/gcore/src/vector/algorithms/intersection.rs +++ b/node-graph/gcore/src/vector/algorithms/intersection.rs @@ -1,4 +1,7 @@ -use kurbo::{BezPath, DEFAULT_ACCURACY, ParamCurve, PathSeg, Shape}; +use glam::DVec2; +use kurbo::{BezPath, DEFAULT_ACCURACY, ParamCurve, PathSeg, Point, Shape}; + +use crate::vector::algorithms::{contants::MAX_ABSOLUTE_DIFFERENCE, util::f64_compare}; use super::contants::MIN_SEPARATION_VALUE; @@ -120,6 +123,33 @@ pub fn filtered_all_segment_intersections(segment1: PathSeg, segment2: PathSeg, }) } +/// Returns the intersection of two lines. The lines are given by a point on the line and its slope (represented by a vector). +pub fn line_intersection(point1: Point, point1_slope_vector: DVec2, point2: Point, point2_slope_vector: DVec2) -> DVec2 { + assert!(point1_slope_vector.normalize() != point2_slope_vector.normalize()); + + // Find the intersection when the first line is vertical + if f64_compare(point1_slope_vector.x, 0., MAX_ABSOLUTE_DIFFERENCE) { + let m2 = point2_slope_vector.y / point2_slope_vector.x; + let b2 = point2.y - m2 * point2.x; + DVec2::new(point1.x, point1.x * m2 + b2) + } + // Find the intersection when the second line is vertical + else if f64_compare(point2_slope_vector.x, 0., MAX_ABSOLUTE_DIFFERENCE) { + let m1 = point1_slope_vector.y / point1_slope_vector.x; + let b1 = point1.y - m1 * point1.x; + DVec2::new(point2.x, point2.x * m1 + b1) + } + // Find the intersection where neither line is vertical + else { + let m1 = point1_slope_vector.y / point1_slope_vector.x; + let b1 = point1.y - m1 * point1.x; + let m2 = point2_slope_vector.y / point2_slope_vector.x; + let b2 = point2.y - m2 * point2.x; + let intersection_x = (b2 - b1) / (m1 - m2); + DVec2::new(intersection_x, intersection_x * m1 + b1) + } +} + #[cfg(test)] mod tests { use super::{bezpath_and_segment_intersections, filtered_segment_intersections}; diff --git a/node-graph/gcore/src/vector/algorithms/util.rs b/node-graph/gcore/src/vector/algorithms/util.rs index 4c396f9ad7..f8ee891e06 100644 --- a/node-graph/gcore/src/vector/algorithms/util.rs +++ b/node-graph/gcore/src/vector/algorithms/util.rs @@ -1,8 +1,8 @@ use super::contants::MAX_ABSOLUTE_DIFFERENCE; use crate::vector::misc::point_to_dvec2; -use glam::BVec2; -use kurbo::Point; +use glam::{BVec2, DVec2}; +use kurbo::{ParamCurve, ParamCurveDeriv, PathSeg, Point}; // Compare two f64s with some maximum absolute difference to account for floating point errors #[cfg(test)] @@ -30,3 +30,18 @@ pub fn compare_vec_of_points(a: Vec, b: Vec, max_absolute_differen pub fn dvec2_compare(a: Point, b: Point, max_abs_diff: f64) -> BVec2 { BVec2::new((a.x - b.x).abs() < max_abs_diff, (a.y - b.y).abs() < max_abs_diff) } + +/// Compare two `f64` numbers with a provided max absolute value difference. +pub fn f64_compare(a: f64, b: f64, max_abs_diff: f64) -> bool { + (a - b).abs() < max_abs_diff +} + +pub fn segment_tangent(segment: PathSeg, t: f64) -> DVec2 { + 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) +} From 2662d16a1a681c981b5758027a301aac713396be Mon Sep 17 00:00:00 2001 From: indierusty Date: Thu, 24 Jul 2025 15:33:44 +0530 Subject: [PATCH 12/17] copy and refactor milter and round join functions from bezier-rs --- .../vector/algorithms/bezpath_algorithms.rs | 49 ++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/node-graph/gcore/src/vector/algorithms/bezpath_algorithms.rs b/node-graph/gcore/src/vector/algorithms/bezpath_algorithms.rs index 00ce32ecc5..ed4e56f25d 100644 --- a/node-graph/gcore/src/vector/algorithms/bezpath_algorithms.rs +++ b/node-graph/gcore/src/vector/algorithms/bezpath_algorithms.rs @@ -1,9 +1,11 @@ +use std::f64::consts::PI; + use super::intersection::{bezpath_intersections, line_intersection}; 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, point_to_dvec2}; -use glam::DVec2; +use glam::{DMat2, DVec2}; use kurbo::{BezPath, CubicBez, DEFAULT_ACCURACY, Line, ParamCurve, ParamCurveDeriv, PathEl, PathSeg, Point, QuadBez, Rect, Shape}; /// Splits the [`BezPath`] at segment index at `t` value which lie in the range of [0, 1]. @@ -415,3 +417,48 @@ pub fn miter_line_join(bezpath1: &BezPath, bezpath2: &BezPath, miter_limit: Opti // If we can't draw the miter join, default to a bevel join None } + +/// Computes the path elements 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 path elements 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() > 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)) +} From 135338be821ed277e4380727cf3bdd8602cc17ec Mon Sep 17 00:00:00 2001 From: indierusty Date: Thu, 24 Jul 2025 16:48:38 +0530 Subject: [PATCH 13/17] it worked! refactor offset path node impl --- .../src/vector/algorithms/offset_subpath.rs | 155 +++++++----------- node-graph/gcore/src/vector/vector_nodes.rs | 28 ++-- 2 files changed, 76 insertions(+), 107 deletions(-) diff --git a/node-graph/gcore/src/vector/algorithms/offset_subpath.rs b/node-graph/gcore/src/vector/algorithms/offset_subpath.rs index 2041ebdefa..189b6c5c3b 100644 --- a/node-graph/gcore/src/vector/algorithms/offset_subpath.rs +++ b/node-graph/gcore/src/vector/algorithms/offset_subpath.rs @@ -1,5 +1,10 @@ -use crate::vector::PointId; -use bezier_rs::{Bezier, BezierHandles, Join, Subpath, TValue}; +use crate::vector::misc::point_to_dvec2; +use kurbo::{BezPath, Join, ParamCurve, PathEl, PathSeg}; + +use super::{ + bezpath_algorithms::{clip_simple_bezpaths, miter_line_join, round_line_join}, + util::segment_tangent, +}; /// Value to control smoothness and mathematical accuracy to offset a cubic Bezier. const CUBIC_REGULARIZATION_ACCURACY: f64 = 0.5; @@ -8,74 +13,48 @@ 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; -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. /// 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() { + info!("not enougn segments"); + 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) + offset_bezpath }) - .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::>>(); + .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::>(); - let mut drop_common_point = vec![true; subpath.len()]; + let mut drop_common_point = vec![true; bezpaths.len()]; // 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.)); + // NOTE: [BezPath] segments are one-indexed. + let out_tangent = segment_tangent(bezpath.get_seg(i + 1).unwrap(), 1.); + let in_tangent = segment_tangent(bezpath.get_seg(j + 1).unwrap(), 0.); let angle = out_tangent.angle_to(in_tangent); // The angle is concave. The Subpath overlap and must be clipped @@ -84,9 +63,9 @@ pub fn offset_subpath(subpath: &Subpath, distance: f64, join: Join) -> // 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; + if let Some((clipped_subpath1, clipped_subpath2)) = clip_simple_bezpaths(bezpath1, bezpath2) { + bezpaths[i] = clipped_subpath1; + bezpaths[j] = clipped_subpath2; apply_join = false; } } @@ -95,36 +74,36 @@ pub fn offset_subpath(subpath: &Subpath, distance: f64, join: 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::Miter => { + let element = miter_line_join(&bezpaths[i], &bezpaths[j], miter_limit); + if let Some(element) = element { + 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 is_bezpath_closed = bezpath.elements().last().is_some_and(|element| *element == PathEl::ClosePath); + if is_bezpath_closed { + let out_tangent = segment_tangent(bezpath.segments().last().unwrap(), 1.); + let in_tangent = segment_tangent(bezpath.segments().next().unwrap(), 0.); let angle = out_tangent.angle_to(in_tangent); 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]) { + if let Some((clipped_subpath1, clipped_subpath2)) = clip_simple_bezpaths(&bezpaths[bezpaths.len() - 1], &bezpaths[0]) { // Merge the clipped subpaths - let last_index = subpaths.len() - 1; - subpaths[last_index] = clipped_subpath1; - subpaths[0] = clipped_subpath2; + let last_index = bezpaths.len() - 1; + bezpaths[last_index] = clipped_subpath1; + bezpaths[0] = clipped_subpath2; apply_join = false; } } @@ -132,42 +111,30 @@ pub fn offset_subpath(subpath: &Subpath, distance: f64, join: 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::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); } } 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; + let segments = bezpaths.iter().flat_map(|bezpath| bezpath.segments().collect::>()).collect::>(); + let mut offset_bezpath = BezPath::from_path_segments(segments.into_iter()); - manipulator_groups.append(&mut manipulators_copy); - } else { - manipulator_groups.append(&mut subpaths[i].manipulator_groups().to_vec()); - } + if is_bezpath_closed { + offset_bezpath.close_path(); } - if subpath.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, subpath.closed) + offset_bezpath } diff --git a/node-graph/gcore/src/vector/vector_nodes.rs b/node-graph/gcore/src/vector/vector_nodes.rs index ca07c37cb8..2ee7e41083 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, evaluate_bezpath, sample_polyline_on_bezpath, split_bezpath, tangent_on_bezpath}; -use super::algorithms::offset_subpath::offset_subpath; +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}; @@ -18,7 +18,7 @@ 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}; @@ -991,10 +991,10 @@ async fn offset_path(_: impl Ctx, vector_data: VectorDataTable, distance: f64, j let mut result_table = VectorDataTable::default(); for mut vector_data_instance in vector_data.instance_iter() { - 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() @@ -1002,24 +1002,26 @@ 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 (i, mut bezpath) in bezpaths.enumerate() { + info!("perfoming offset for bezpath {}", i); + 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; From 7a49adf164a02149fc90edb028bcb2bec251783e Mon Sep 17 00:00:00 2001 From: indierusty Date: Sun, 27 Jul 2025 11:50:19 +0530 Subject: [PATCH 14/17] fix few bugs --- .../vector/algorithms/bezpath_algorithms.rs | 52 ++++++------ .../src/vector/algorithms/offset_subpath.rs | 84 +++++++++---------- .../gcore/src/vector/algorithms/util.rs | 3 + 3 files changed, 69 insertions(+), 70 deletions(-) diff --git a/node-graph/gcore/src/vector/algorithms/bezpath_algorithms.rs b/node-graph/gcore/src/vector/algorithms/bezpath_algorithms.rs index ed4e56f25d..d4fd01da53 100644 --- a/node-graph/gcore/src/vector/algorithms/bezpath_algorithms.rs +++ b/node-graph/gcore/src/vector/algorithms/bezpath_algorithms.rs @@ -1,10 +1,13 @@ +use std::f64; use std::f64::consts::PI; -use super::intersection::{bezpath_intersections, line_intersection}; +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, point_to_dvec2}; + use glam::{DMat2, DVec2}; use kurbo::{BezPath, CubicBez, DEFAULT_ACCURACY, Line, ParamCurve, ParamCurveDeriv, PathEl, PathSeg, Point, QuadBez, Rect, Shape}; @@ -374,14 +377,14 @@ pub fn clip_simple_bezpaths(bezpath1: &BezPath, bezpath2: &BezPath) -> Option<(B /// Alternatively, this can be interpreted as limiting the angle that the miter can form. /// When the limit is exceeded, no manipulator group 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 { +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.get_seg(0)?; + let in_segment = bezpath1.segments().last().unwrap(); + let out_segment = bezpath2.segments().next().unwrap(); let in_tangent = segment_tangent(in_segment, 1.); let out_tangent = segment_tangent(out_segment, 0.); @@ -391,31 +394,26 @@ pub fn miter_line_join(bezpath1: &BezPath, bezpath2: &BezPath, miter_limit: Opti // TODO: Besides returning None, is there a more appropriate way to handle this? return None; } - let normalized_in_tangent = in_tangent.normalize(); - let normalized_out_tangent = out_tangent.normalize(); - - // The tangents must not be parallel for the miter join - if !normalized_in_tangent.abs_diff_eq(normalized_out_tangent, MAX_ABSOLUTE_DIFFERENCE) && !normalized_in_tangent.abs_diff_eq(-normalized_out_tangent, MAX_ABSOLUTE_DIFFERENCE) { - let intersection = line_intersection(in_segment.end(), in_tangent, out_segment.start(), out_tangent); - - let start_to_intersection = intersection - point_to_dvec2(in_segment.end()); - let intersection_to_end = point_to_dvec2(out_segment.start()) - intersection; - if start_to_intersection == DVec2::ZERO || intersection_to_end == DVec2::ZERO { - // Avoid panic from normalizing zero vectors - // TODO: Besides returning None, is there a more appropriate way to handle this? - return None; - } - // Draw the miter join if the intersection occurs in the correct direction with respect to the path - if start_to_intersection.normalize().abs_diff_eq(in_tangent, MAX_ABSOLUTE_DIFFERENCE) - && intersection_to_end.normalize().abs_diff_eq(out_tangent, MAX_ABSOLUTE_DIFFERENCE) - && miter_limit > f64::EPSILON / (start_to_intersection.angle_to(-intersection_to_end).abs() / 2.).sin() - { - return Some(PathEl::LineTo(dvec2_to_point(intersection))); - } + let angle = (in_tangent * -1.).angle_to(out_tangent).abs(); + + if angle.to_degrees() < miter_limit { + return None; } - // If we can't draw the miter join, default to a bevel join - 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 interseciton point to draw the miter join, default to a bevel join + // otherwise, return the element to create the join. + let intersection = line1.crossing_point(line2)?; + + return Some([PathEl::LineTo(intersection), PathEl::LineTo(out_segment.start())]); } /// Computes the path elements to form a circular join from `left` to `right`, along a circle around `center`. diff --git a/node-graph/gcore/src/vector/algorithms/offset_subpath.rs b/node-graph/gcore/src/vector/algorithms/offset_subpath.rs index 189b6c5c3b..a7f42e6fb5 100644 --- a/node-graph/gcore/src/vector/algorithms/offset_subpath.rs +++ b/node-graph/gcore/src/vector/algorithms/offset_subpath.rs @@ -1,26 +1,21 @@ use crate::vector::misc::point_to_dvec2; use kurbo::{BezPath, Join, ParamCurve, PathEl, PathSeg}; -use super::{ - bezpath_algorithms::{clip_simple_bezpaths, miter_line_join, round_line_join}, - util::segment_tangent, -}; +use super::bezpath_algorithms::{clip_simple_bezpaths, miter_line_join, round_line_join}; /// 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; -// 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_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. || bezpath.get_seg(1).is_none() { - info!("not enougn segments"); return bezpath.clone(); } @@ -35,8 +30,6 @@ pub fn offset_bezpath(bezpath: &BezPath, distance: f64, join: Join, miter_limit: .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::>(); - let mut drop_common_point = vec![true; bezpaths.len()]; - // Clip or join consecutive Subpaths for i in 0..bezpaths.len() - 1 { let j = i + 1; @@ -51,32 +44,28 @@ pub fn offset_bezpath(bezpath: &BezPath, distance: f64, join: Join, miter_limit: continue; } - // Calculate the angle formed between two consecutive Subpaths - // NOTE: [BezPath] segments are one-indexed. - let out_tangent = segment_tangent(bezpath.get_seg(i + 1).unwrap(), 1.); - let in_tangent = segment_tangent(bezpath.get_seg(j + 1).unwrap(), 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)) = clip_simple_bezpaths(bezpath1, bezpath2) { - bezpaths[i] = clipped_subpath1; - bezpaths[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::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); } } @@ -93,28 +82,30 @@ pub fn offset_bezpath(bezpath: &BezPath, distance: f64, join: Join, miter_limit: // Clip any overlap in the last segment let is_bezpath_closed = bezpath.elements().last().is_some_and(|element| *element == PathEl::ClosePath); if is_bezpath_closed { - let out_tangent = segment_tangent(bezpath.segments().last().unwrap(), 1.); - let in_tangent = segment_tangent(bezpath.segments().next().unwrap(), 0.); - let angle = out_tangent.angle_to(in_tangent); - let mut apply_join = true; - if (angle > 0. && distance > 0.) || (angle < 0. && distance < 0.) { - 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 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::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); } } @@ -129,12 +120,19 @@ pub fn offset_bezpath(bezpath: &BezPath, distance: f64, join: Join, miter_limit: } } - // Merge the subpaths. Drop points which overlap with one another. + // 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 = BezPath::from_path_segments(segments.into_iter()); + let mut offset_bezpath = segments.iter().fold(BezPath::new(), |mut acc, segment| { + if acc.elements().is_empty() { + acc.move_to(segment.start()); + } + acc.push(segment.as_path_el()); + acc + }); if is_bezpath_closed { offset_bezpath.close_path(); } + offset_bezpath } diff --git a/node-graph/gcore/src/vector/algorithms/util.rs b/node-graph/gcore/src/vector/algorithms/util.rs index f8ee891e06..7ea705e1be 100644 --- a/node-graph/gcore/src/vector/algorithms/util.rs +++ b/node-graph/gcore/src/vector/algorithms/util.rs @@ -37,6 +37,9 @@ pub fn f64_compare(a: f64, b: f64, max_abs_diff: f64) -> bool { } 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), From bf94f51114dfc69bf58891c46912e1f4d977f127 Mon Sep 17 00:00:00 2001 From: indierusty Date: Sun, 27 Jul 2025 12:12:25 +0530 Subject: [PATCH 15/17] improve vars names and add comments --- .../vector/algorithms/bezpath_algorithms.rs | 8 +-- .../src/vector/algorithms/intersection.rs | 58 +++++-------------- 2 files changed, 18 insertions(+), 48 deletions(-) diff --git a/node-graph/gcore/src/vector/algorithms/bezpath_algorithms.rs b/node-graph/gcore/src/vector/algorithms/bezpath_algorithms.rs index d4fd01da53..594a12014a 100644 --- a/node-graph/gcore/src/vector/algorithms/bezpath_algorithms.rs +++ b/node-graph/gcore/src/vector/algorithms/bezpath_algorithms.rs @@ -371,11 +371,11 @@ pub fn clip_simple_bezpaths(bezpath1: &BezPath, bezpath2: &BezPath) -> Option<(B Some((clipped_subpath1, clipped_subpath2)) } -/// Returns the manipulator point that is needed for a miter join if it is possible. +/// 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 manipulator group will be returned. +/// 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 { @@ -416,7 +416,7 @@ pub fn miter_line_join(bezpath1: &BezPath, bezpath2: &BezPath, miter_limit: Opti return Some([PathEl::LineTo(intersection), PathEl::LineTo(out_segment.start())]); } -/// Computes the path elements to form a circular join from `left` to `right`, along a circle around `center`. +/// 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; @@ -439,7 +439,7 @@ pub fn compute_circular_subpath_details(left: DVec2, arc_point: DVec2, right: DV [first_half, second_half] } -/// Returns path elements to create a round join with the provided center. +/// 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()); diff --git a/node-graph/gcore/src/vector/algorithms/intersection.rs b/node-graph/gcore/src/vector/algorithms/intersection.rs index c882f3107d..d8efaba0ba 100644 --- a/node-graph/gcore/src/vector/algorithms/intersection.rs +++ b/node-graph/gcore/src/vector/algorithms/intersection.rs @@ -1,13 +1,10 @@ -use glam::DVec2; -use kurbo::{BezPath, DEFAULT_ACCURACY, ParamCurve, PathSeg, Point, Shape}; - -use crate::vector::algorithms::{contants::MAX_ABSOLUTE_DIFFERENCE, util::f64_compare}; +use kurbo::{BezPath, DEFAULT_ACCURACY, ParamCurve, PathSeg, Shape}; use super::contants::MIN_SEPARATION_VALUE; -/// 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 occurred. +/// 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`: the minimum difference 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 @@ -33,6 +30,7 @@ pub fn bezpath_intersections(bezpath1: &BezPath, bezpath2: &BezPath, accuracy: O 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); @@ -47,6 +45,8 @@ pub fn segment_intersections(segment1: PathSeg, segment2: PathSeg, accuracy: Opt } } +/// Implements [https://pomax.github.io/bezierinfo/#curveintersection] to find intersection between two Bezier segments +/// by splitting the segment recursively until the size of the subsegments bounding box is smaller than the accuracy. 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(); @@ -56,17 +56,18 @@ fn segment_intersections_inner(segment1: PathSeg, min_t1: f64, max_t1: f64, segm // Check if the bounding boxes overlap if bbox1.overlaps(bbox2) { - // If bounding boxes are within the error threshold (i.e. are small enough), we have found an intersection + // If bounding boxes overlaps 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 and repeat with the combinations of the two halves of each curve + // 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); @@ -75,16 +76,14 @@ fn segment_intersections_inner(segment1: PathSeg, min_t1: f64, max_t1: f64, segm } // 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 +/// 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, 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. +/// 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` - For intersections where the provided bezier segment is non-linear, `accuracy` defines the maximum size of the bounding boxes to be considered an intersection point. /// - `minimum_separation` - 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 { - // TODO: Consider using the `intersections_between_vectors_of_curves` helper function here - // Otherwise, use bounding box to determine intersections let mut intersection_t_values = segment_intersections(segment1, segment2, accuracy); intersection_t_values.sort_by(|a, b| a.partial_cmp(b).unwrap()); @@ -106,8 +105,6 @@ pub fn filtered_segment_intersections(segment1: PathSeg, segment2: PathSeg, accu /// - `error` - For intersections where the provided bezier is non-linear, `error` defines the threshold for bounding boxes to be considered an intersection point. /// - `minimum_separation` - 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)> { - // TODO: Consider using the `intersections_between_vectors_of_curves` helper function here - // Otherwise, use bounding box to determine intersections 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()); @@ -123,33 +120,6 @@ pub fn filtered_all_segment_intersections(segment1: PathSeg, segment2: PathSeg, }) } -/// Returns the intersection of two lines. The lines are given by a point on the line and its slope (represented by a vector). -pub fn line_intersection(point1: Point, point1_slope_vector: DVec2, point2: Point, point2_slope_vector: DVec2) -> DVec2 { - assert!(point1_slope_vector.normalize() != point2_slope_vector.normalize()); - - // Find the intersection when the first line is vertical - if f64_compare(point1_slope_vector.x, 0., MAX_ABSOLUTE_DIFFERENCE) { - let m2 = point2_slope_vector.y / point2_slope_vector.x; - let b2 = point2.y - m2 * point2.x; - DVec2::new(point1.x, point1.x * m2 + b2) - } - // Find the intersection when the second line is vertical - else if f64_compare(point2_slope_vector.x, 0., MAX_ABSOLUTE_DIFFERENCE) { - let m1 = point1_slope_vector.y / point1_slope_vector.x; - let b1 = point1.y - m1 * point1.x; - DVec2::new(point2.x, point2.x * m1 + b1) - } - // Find the intersection where neither line is vertical - else { - let m1 = point1_slope_vector.y / point1_slope_vector.x; - let b1 = point1.y - m1 * point1.x; - let m2 = point2_slope_vector.y / point2_slope_vector.x; - let b2 = point2.y - m2 * point2.x; - let intersection_x = (b2 - b1) / (m1 - m2); - DVec2::new(intersection_x, intersection_x * m1 + b1) - } -} - #[cfg(test)] mod tests { use super::{bezpath_and_segment_intersections, filtered_segment_intersections}; From d45e4e93773ad59e81b61b823327d7354467e967 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Thu, 31 Jul 2025 17:15:25 -0700 Subject: [PATCH 16/17] Code review --- .../vector/algorithms/bezpath_algorithms.rs | 40 +++++++------- .../src/vector/algorithms/intersection.rs | 33 +++++++----- .../src/vector/algorithms/offset_subpath.rs | 7 ++- .../gcore/src/vector/algorithms/util.rs | 54 +++++++++---------- node-graph/gcore/src/vector/vector_nodes.rs | 1 - 5 files changed, 64 insertions(+), 71 deletions(-) diff --git a/node-graph/gcore/src/vector/algorithms/bezpath_algorithms.rs b/node-graph/gcore/src/vector/algorithms/bezpath_algorithms.rs index 594a12014a..bbeac028e2 100644 --- a/node-graph/gcore/src/vector/algorithms/bezpath_algorithms.rs +++ b/node-graph/gcore/src/vector/algorithms/bezpath_algorithms.rs @@ -1,15 +1,11 @@ -use std::f64; -use std::f64::consts::PI; - 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, 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 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. @@ -59,7 +55,7 @@ pub fn split_bezpath_at_segment(bezpath: &BezPath, segment_index: usize, t: f64) Some((first_bezpath, second_bezpath)) } -/// Splits the [`BezPath`] at `t` value which lie in the range of [0, 1]. +/// 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 { @@ -348,32 +344,32 @@ pub fn is_linear(segment: &PathSeg) -> bool { } // 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 Bezpaths. Returns an optional, as 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. +// 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 intersections1 = bezpath_intersections(bezpath1, bezpath2, None, None); - if intersections1.is_empty() { + let subpath_1_intersections = bezpath_intersections(bezpath1, bezpath2, None, None); + if subpath_1_intersections.is_empty() { return None; } - let (segment_index, t) = *intersections1.last().unwrap(); + 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 intersections2 = bezpath_intersections(bezpath2, bezpath1, None, None); - if intersections2.is_empty() { + let subpath_2_intersections = bezpath_intersections(bezpath2, bezpath1, None, None); + if subpath_2_intersections.is_empty() { return None; } - let (segment_index, t) = intersections2[0]; + 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. /// +/// `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. @@ -383,8 +379,8 @@ pub fn miter_line_join(bezpath1: &BezPath, bezpath2: &BezPath, miter_limit: Opti _ => 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().unwrap(); - let out_segment = bezpath2.segments().next().unwrap(); + 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.); @@ -409,11 +405,11 @@ pub fn miter_line_join(bezpath1: &BezPath, bezpath2: &BezPath, miter_limit: Opti let p2 = point_to_dvec2(p1) + out_tangent.normalize(); let line2 = Line::new(p1, dvec2_to_point(p2)); - // If we don't find the interseciton point to draw the miter join, default to a bevel join - // otherwise, return the element to create the join. + // 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)?; - return Some([PathEl::LineTo(intersection), PathEl::LineTo(out_segment.start())]); + 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`. @@ -453,7 +449,7 @@ pub fn round_line_join(bezpath1: &BezPath, bezpath2: &BezPath, center: DVec2) -> 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() > PI / 2. { + 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); } diff --git a/node-graph/gcore/src/vector/algorithms/intersection.rs b/node-graph/gcore/src/vector/algorithms/intersection.rs index d8efaba0ba..de373aa1b8 100644 --- a/node-graph/gcore/src/vector/algorithms/intersection.rs +++ b/node-graph/gcore/src/vector/algorithms/intersection.rs @@ -1,11 +1,11 @@ -use kurbo::{BezPath, DEFAULT_ACCURACY, ParamCurve, PathSeg, Shape}; - 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`: the minimum difference two adjacent `t`-values must have when comparing adjacent `t`-values in sorted order. +/// +/// `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() @@ -46,7 +46,8 @@ pub fn segment_intersections(segment1: PathSeg, segment2: PathSeg, accuracy: Opt } /// Implements [https://pomax.github.io/bezierinfo/#curveintersection] to find intersection between two Bezier segments -/// by splitting the segment recursively until the size of the subsegments bounding box is smaller than the accuracy. +/// 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(); @@ -56,14 +57,14 @@ fn segment_intersections_inner(segment1: PathSeg, min_t1: f64, max_t1: f64, segm // Check if the bounding boxes overlap if bbox1.overlaps(bbox2) { - // If bounding boxes overlaps and they are small enough, we have found an intersection + // 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. + // Use the middle `t` value, append the corresponding `t` value intersections.push((mid_t1, mid_t2)); return; } - // Split curves in half. + // Split curves in half let (seg11, seg12) = segment1.subdivide(); let (seg21, seg22) = segment2.subdivide(); @@ -81,8 +82,10 @@ fn segment_intersections_inner(segment1: PathSeg, min_t1: f64, max_t1: f64, segm /// 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` - For intersections where the provided bezier segment is non-linear, `accuracy` defines the maximum size of the bounding boxes to be considered an intersection point. -/// - `minimum_separation` - The minimum difference between adjacent `t` values in sorted order +/// +/// `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()); @@ -97,13 +100,15 @@ pub fn filtered_segment_intersections(segment1: PathSeg, segment2: PathSeg, accu } // 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 2 adjacent `t` values is less than the minimum difference, the filtering takes the larger `t` value and discards the smaller `t` value. +/// 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, `error` defines the threshold for bounding boxes to be considered an intersection point. -/// - `minimum_separation` - The minimum difference between adjacent `t` values in sorted order +/// +/// `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()); diff --git a/node-graph/gcore/src/vector/algorithms/offset_subpath.rs b/node-graph/gcore/src/vector/algorithms/offset_subpath.rs index a7f42e6fb5..5ed0ba89d0 100644 --- a/node-graph/gcore/src/vector/algorithms/offset_subpath.rs +++ b/node-graph/gcore/src/vector/algorithms/offset_subpath.rs @@ -1,8 +1,7 @@ +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}; -use super::bezpath_algorithms::{clip_simple_bezpaths, miter_line_join, round_line_join}; - /// 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. @@ -24,8 +23,8 @@ pub fn offset_bezpath(bezpath: &BezPath, distance: f64, join: Join, miter_limit: .map(|bezier| bezier.to_cubic()) .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); - offset_bezpath + + kurbo::fit_to_bezpath(&cubic_offset, CUBIC_TO_BEZPATH_ACCURACY) }) .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::>(); diff --git a/node-graph/gcore/src/vector/algorithms/util.rs b/node-graph/gcore/src/vector/algorithms/util.rs index 7ea705e1be..5f70428abf 100644 --- a/node-graph/gcore/src/vector/algorithms/util.rs +++ b/node-graph/gcore/src/vector/algorithms/util.rs @@ -1,50 +1,44 @@ -use super::contants::MAX_ABSOLUTE_DIFFERENCE; -use crate::vector::misc::point_to_dvec2; +use glam::DVec2; +use kurbo::{ParamCurve, ParamCurveDeriv, PathSeg}; -use glam::{BVec2, DVec2}; -use kurbo::{ParamCurve, ParamCurveDeriv, PathSeg, Point}; +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() < MAX_ABSOLUTE_DIFFERENCE + (f1 - f2).abs() < super::contants::MAX_ABSOLUTE_DIFFERENCE } /// Compare points by allowing some maximum absolute difference to account for floating point errors -pub fn compare_points(p1: Point, p2: Point) -> bool { - let (p1, p2) = (point_to_dvec2(p1), point_to_dvec2(p2)); - p1.abs_diff_eq(p2, MAX_ABSOLUTE_DIFFERENCE) +#[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 { +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)| (point_to_dvec2(p1), point_to_dvec2(p2))) + .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. -pub fn dvec2_compare(a: Point, b: Point, max_abs_diff: f64) -> BVec2 { - BVec2::new((a.x - b.x).abs() < max_abs_diff, (a.y - b.y).abs() < max_abs_diff) -} - -/// Compare two `f64` numbers with a provided max absolute value difference. -pub fn f64_compare(a: f64, b: f64, max_abs_diff: f64) -> bool { - (a - b).abs() < max_abs_diff -} - -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) +#[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 7845577c85..85d6ec503e 100644 --- a/node-graph/gcore/src/vector/vector_nodes.rs +++ b/node-graph/gcore/src/vector/vector_nodes.rs @@ -17,7 +17,6 @@ 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, ManipulatorGroup, Subpath}; use core::f64::consts::PI; use core::hash::{Hash, Hasher}; From 70b023cafaabbb6c2831cb42f08ce1eb2e7e56f8 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Thu, 31 Jul 2025 17:23:33 -0700 Subject: [PATCH 17/17] fmt --- node-graph/gcore/src/vector/algorithms/contants.rs | 1 + node-graph/gcore/src/vector/algorithms/offset_subpath.rs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/node-graph/gcore/src/vector/algorithms/contants.rs b/node-graph/gcore/src/vector/algorithms/contants.rs index c970059fa7..021e49be22 100644 --- a/node-graph/gcore/src/vector/algorithms/contants.rs +++ b/node-graph/gcore/src/vector/algorithms/contants.rs @@ -2,4 +2,5 @@ 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/offset_subpath.rs b/node-graph/gcore/src/vector/algorithms/offset_subpath.rs index 5ed0ba89d0..776a06a1db 100644 --- a/node-graph/gcore/src/vector/algorithms/offset_subpath.rs +++ b/node-graph/gcore/src/vector/algorithms/offset_subpath.rs @@ -23,7 +23,7 @@ pub fn offset_bezpath(bezpath: &BezPath, distance: f64, join: Join, miter_limit: .map(|bezier| bezier.to_cubic()) .map(|cubic_bez| { let cubic_offset = kurbo::offset::CubicOffset::new_regularized(cubic_bez, distance, CUBIC_REGULARIZATION_ACCURACY); - + kurbo::fit_to_bezpath(&cubic_offset, CUBIC_TO_BEZPATH_ACCURACY) }) .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).