Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions libraries/bezier-rs/src/subpath/solvers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -437,7 +437,7 @@ impl<PointId: crate::Identifier> Subpath<PointId> {
/// 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<PointId>, miter_limit: Option<f64>) -> Option<ManipulatorGroup<PointId>> {
pub fn miter_line_join(&self, other: &Subpath<PointId>, miter_limit: Option<f64>) -> Option<ManipulatorGroup<PointId>> {
let miter_limit = match miter_limit {
Some(miter_limit) if miter_limit > f64::EPSILON => miter_limit,
_ => 4.,
Expand Down Expand Up @@ -491,7 +491,7 @@ impl<PointId: crate::Identifier> Subpath<PointId> {
/// - 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<PointId>, center: DVec2) -> (DVec2, ManipulatorGroup<PointId>, DVec2) {
pub fn round_line_join(&self, other: &Subpath<PointId>, center: DVec2) -> (DVec2, ManipulatorGroup<PointId>, DVec2) {
let left = self.manipulator_groups[self.len() - 1].anchor;
let right = other.manipulator_groups[0].anchor;

Expand Down
4 changes: 3 additions & 1 deletion libraries/bezier-rs/src/subpath/transform.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand Down Expand Up @@ -307,7 +308,7 @@ impl<PointId: crate::Identifier> Subpath<PointId> {
// 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<PointId>, subpath2: &Subpath<PointId>) -> Option<(Subpath<PointId>, Subpath<PointId>)> {
pub fn clip_simple_subpaths(subpath1: &Subpath<PointId>, subpath2: &Subpath<PointId>) -> Option<(Subpath<PointId>, Subpath<PointId>)> {
// Split the first subpath at its last intersection
let intersections1 = subpath1.subpath_intersections(subpath2, None, None);
if intersections1.is_empty() {
Expand Down Expand Up @@ -366,6 +367,7 @@ impl<PointId: crate::Identifier> Subpath<PointId> {
.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::<Vec<Subpath<PointId>>>();

let mut drop_common_point = vec![true; self.len()];

// Clip or join consecutive Subpaths
Expand Down
1 change: 1 addition & 0 deletions node-graph/gcore/src/vector/algorithms/mod.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
mod instance;
mod merge_by_distance;
pub mod offset_subpath;
173 changes: 173 additions & 0 deletions node-graph/gcore/src/vector/algorithms/offset_subpath.rs
Original file line number Diff line number Diff line change
@@ -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<PointId>, distance: f64, join: Join) -> Subpath<PointId> {
// 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::<Vec<Subpath<PointId>>>();

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)
}
4 changes: 3 additions & 1 deletion node-graph/gcore/src/vector/vector_nodes.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand Down Expand Up @@ -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)),
Expand Down
Loading