diff --git a/libraries/bezier-rs/src/subpath/solvers.rs b/libraries/bezier-rs/src/subpath/solvers.rs index 0058eccbea..d3558fc22d 100644 --- a/libraries/bezier-rs/src/subpath/solvers.rs +++ b/libraries/bezier-rs/src/subpath/solvers.rs @@ -437,7 +437,7 @@ impl Subpath { /// 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(crate) fn miter_line_join(&self, other: &Subpath, miter_limit: Option) -> Option> { + pub fn miter_line_join(&self, other: &Subpath, miter_limit: Option) -> Option> { let miter_limit = match miter_limit { Some(miter_limit) if miter_limit > f64::EPSILON => miter_limit, _ => 4., @@ -491,7 +491,7 @@ impl Subpath { /// - The `out_handle` for the last manipulator group of `self` /// - The new manipulator group to be added /// - The `in_handle` for the first manipulator group of `other` - pub(crate) fn round_line_join(&self, other: &Subpath, center: DVec2) -> (DVec2, ManipulatorGroup, DVec2) { + pub fn round_line_join(&self, other: &Subpath, center: DVec2) -> (DVec2, ManipulatorGroup, DVec2) { let left = self.manipulator_groups[self.len() - 1].anchor; let right = other.manipulator_groups[0].anchor; diff --git a/libraries/bezier-rs/src/subpath/transform.rs b/libraries/bezier-rs/src/subpath/transform.rs index 20b2fd8575..88fc4612f3 100644 --- a/libraries/bezier-rs/src/subpath/transform.rs +++ b/libraries/bezier-rs/src/subpath/transform.rs @@ -1,4 +1,5 @@ use super::*; +use crate::BezierHandles; use crate::consts::MAX_ABSOLUTE_DIFFERENCE; use crate::utils::{Cap, Join, SubpathTValue, TValue}; use glam::{DAffine2, DVec2}; @@ -307,7 +308,7 @@ impl Subpath { // at the incorrect location. This can be avoided by first trimming the two Subpaths at any extrema, effectively ignoring loopbacks. /// Helper function to clip overlap of two intersecting open Subpaths. Returns an optional, as intersections may not exist for certain arrangements and distances. /// Assumes that the Subpaths represents simple Bezier segments, and clips the Subpaths at the last intersection of the first Subpath, and first intersection of the last Subpath. - fn clip_simple_subpaths(subpath1: &Subpath, subpath2: &Subpath) -> Option<(Subpath, Subpath)> { + pub fn clip_simple_subpaths(subpath1: &Subpath, subpath2: &Subpath) -> Option<(Subpath, Subpath)> { // Split the first subpath at its last intersection let intersections1 = subpath1.subpath_intersections(subpath2, None, None); if intersections1.is_empty() { @@ -366,6 +367,7 @@ impl Subpath { .map(|bezier| bezier.offset(distance)) .filter(|subpath| subpath.len() >= 2) // In some cases the reduced and scaled bézier is marked by is_point (so the subpath is empty). .collect::>>(); + let mut drop_common_point = vec![true; self.len()]; // Clip or join consecutive Subpaths diff --git a/node-graph/gcore/src/vector/algorithms/mod.rs b/node-graph/gcore/src/vector/algorithms/mod.rs index 2cd7217c02..a03cf7c6af 100644 --- a/node-graph/gcore/src/vector/algorithms/mod.rs +++ b/node-graph/gcore/src/vector/algorithms/mod.rs @@ -1,2 +1,3 @@ mod instance; mod merge_by_distance; +pub mod offset_subpath; diff --git a/node-graph/gcore/src/vector/algorithms/offset_subpath.rs b/node-graph/gcore/src/vector/algorithms/offset_subpath.rs new file mode 100644 index 0000000000..daaf23e924 --- /dev/null +++ b/node-graph/gcore/src/vector/algorithms/offset_subpath.rs @@ -0,0 +1,173 @@ +use crate::vector::PointId; +use bezier_rs::{Bezier, BezierHandles, Join, Subpath, TValue}; + +/// 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; + +fn segment_to_bezier(seg: kurbo::PathSeg) -> bezier_rs::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 { + // 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(); + } + + let mut subpaths = subpath + .iter() + .filter(|bezier| !bezier.is_point()) + .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)); + 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) + }) + .filter(|subpath| subpath.len() >= 2) // In some cases the reduced and scaled bézier is marked by is_point (so the subpath is empty). + .collect::>>(); + + let mut drop_common_point = vec![true; subpath.len()]; + + // Clip or join consecutive Subpaths + for i in 0..subpaths.len() - 1 { + let j = i + 1; + let subpath1 = &subpaths[i]; + let subpath2 = &subpaths[j]; + + let last_segment = subpath1.get_segment(subpath1.len_segments() - 1).unwrap(); + let first_segment = subpath2.get_segment(0).unwrap(); + + // If the anchors are approximately equal, there is no need to clip / join the segments + if last_segment.end().abs_diff_eq(first_segment.start(), MAX_ABSOLUTE_DIFFERENCE) { + continue; + } + + // Calculate the angle formed between two consecutive Subpaths + let out_tangent = subpath.get_segment(i).unwrap().tangent(TValue::Parametric(1.)); + let in_tangent = subpath.get_segment(j).unwrap().tangent(TValue::Parametric(0.)); + let angle = out_tangent.angle_to(in_tangent); + + // The angle is concave. The Subpath overlap and must be clipped + let mut apply_join = true; + if (angle > 0. && distance > 0.) || (angle < 0. && distance < 0.) { + // If the distance is large enough, there may still be no intersections. Also, if the angle is close enough to zero, + // subpath intersections may find no intersections. In this case, the points are likely close enough that we can approximate + // the points as being on top of one another. + if let Some((clipped_subpath1, clipped_subpath2)) = Subpath::clip_simple_subpaths(subpath1, subpath2) { + subpaths[i] = clipped_subpath1; + subpaths[j] = clipped_subpath2; + apply_join = false; + } + } + // The angle is convex. The Subpath must be joined using the specified join type + if apply_join { + drop_common_point[j] = false; + match join { + Join::Bevel => {} + Join::Miter(miter_limit) => { + let miter_manipulator_group = subpaths[i].miter_line_join(&subpaths[j], miter_limit); + if let Some(miter_manipulator_group) = miter_manipulator_group { + subpaths[i].manipulator_groups_mut().push(miter_manipulator_group); + } + } + Join::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); + } + } + } + } + + // Clip any overlap in the last segment + if subpath.closed { + let out_tangent = subpath.get_segment(subpath.len_segments() - 1).unwrap().tangent(TValue::Parametric(1.)); + let in_tangent = subpath.get_segment(0).unwrap().tangent(TValue::Parametric(0.)); + let angle = out_tangent.angle_to(in_tangent); + + let mut apply_join = true; + if (angle > 0. && distance > 0.) || (angle < 0. && distance < 0.) { + if let Some((clipped_subpath1, clipped_subpath2)) = Subpath::clip_simple_subpaths(&subpaths[subpaths.len() - 1], &subpaths[0]) { + // Merge the clipped subpaths + let last_index = subpaths.len() - 1; + subpaths[last_index] = clipped_subpath1; + subpaths[0] = clipped_subpath2; + apply_join = false; + } + } + if apply_join { + drop_common_point[0] = false; + match join { + Join::Bevel => {} + Join::Miter(miter_limit) => { + let last_subpath_index = subpaths.len() - 1; + let miter_manipulator_group = subpaths[last_subpath_index].miter_line_join(&subpaths[0], miter_limit); + if let Some(miter_manipulator_group) = miter_manipulator_group { + subpaths[last_subpath_index].manipulator_groups_mut().push(miter_manipulator_group); + } + } + Join::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); + } + } + } + } + + // Merge the subpaths. Drop points which overlap with one another. + let mut manipulator_groups = subpaths[0].manipulator_groups().to_vec(); + for i in 1..subpaths.len() { + if drop_common_point[i] { + let last_group = manipulator_groups.pop().unwrap(); + let mut manipulators_copy = subpaths[i].manipulator_groups().to_vec(); + manipulators_copy[0].in_handle = last_group.in_handle; + + manipulator_groups.append(&mut manipulators_copy); + } else { + manipulator_groups.append(&mut subpaths[i].manipulator_groups().to_vec()); + } + } + 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) +} diff --git a/node-graph/gcore/src/vector/vector_nodes.rs b/node-graph/gcore/src/vector/vector_nodes.rs index 482f4ae7da..d0fda86e8d 100644 --- a/node-graph/gcore/src/vector/vector_nodes.rs +++ b/node-graph/gcore/src/vector/vector_nodes.rs @@ -1,3 +1,4 @@ +use super::algorithms::offset_subpath::offset_subpath; use super::misc::CentroidType; use super::style::{Fill, Gradient, GradientStops, Stroke}; use super::{PointId, SegmentDomain, SegmentId, StrokeId, VectorData, VectorDataTable}; @@ -993,7 +994,8 @@ async fn offset_path(_: impl Ctx, vector_data: VectorDataTable, distance: f64, l subpath.apply_transform(vector_data_transform); // Taking the existing stroke data and passing it to Bezier-rs to generate new paths. - let mut subpath_out = subpath.offset( + let mut subpath_out = offset_subpath( + &subpath, -distance, match line_join { LineJoin::Miter => Join::Miter(Some(miter_limit)),