Skip to content

Commit dd1feee

Browse files
indierustyindierustyKeavon
authored
Replace Bezier-rs use in the 'Offset Path' node with a Kurbo algorithm (#2596)
* minimally replace bezier-rs use in Offset Path node implementation with kurbo's API * fix kurbo import * refactor * Code review --------- Co-authored-by: indierusty <[email protected]> Co-authored-by: Keavon Chambers <[email protected]>
1 parent a376832 commit dd1feee

File tree

5 files changed

+182
-4
lines changed

5 files changed

+182
-4
lines changed

libraries/bezier-rs/src/subpath/solvers.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -437,7 +437,7 @@ impl<PointId: crate::Identifier> Subpath<PointId> {
437437
/// Alternatively, this can be interpreted as limiting the angle that the miter can form.
438438
/// When the limit is exceeded, no manipulator group will be returned.
439439
/// This value should be greater than 0. If not, the default of 4 will be used.
440-
pub(crate) fn miter_line_join(&self, other: &Subpath<PointId>, miter_limit: Option<f64>) -> Option<ManipulatorGroup<PointId>> {
440+
pub fn miter_line_join(&self, other: &Subpath<PointId>, miter_limit: Option<f64>) -> Option<ManipulatorGroup<PointId>> {
441441
let miter_limit = match miter_limit {
442442
Some(miter_limit) if miter_limit > f64::EPSILON => miter_limit,
443443
_ => 4.,
@@ -491,7 +491,7 @@ impl<PointId: crate::Identifier> Subpath<PointId> {
491491
/// - The `out_handle` for the last manipulator group of `self`
492492
/// - The new manipulator group to be added
493493
/// - The `in_handle` for the first manipulator group of `other`
494-
pub(crate) fn round_line_join(&self, other: &Subpath<PointId>, center: DVec2) -> (DVec2, ManipulatorGroup<PointId>, DVec2) {
494+
pub fn round_line_join(&self, other: &Subpath<PointId>, center: DVec2) -> (DVec2, ManipulatorGroup<PointId>, DVec2) {
495495
let left = self.manipulator_groups[self.len() - 1].anchor;
496496
let right = other.manipulator_groups[0].anchor;
497497

libraries/bezier-rs/src/subpath/transform.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use super::*;
2+
use crate::BezierHandles;
23
use crate::consts::MAX_ABSOLUTE_DIFFERENCE;
34
use crate::utils::{Cap, Join, SubpathTValue, TValue};
45
use glam::{DAffine2, DVec2};
@@ -307,7 +308,7 @@ impl<PointId: crate::Identifier> Subpath<PointId> {
307308
// at the incorrect location. This can be avoided by first trimming the two Subpaths at any extrema, effectively ignoring loopbacks.
308309
/// Helper function to clip overlap of two intersecting open Subpaths. Returns an optional, as intersections may not exist for certain arrangements and distances.
309310
/// 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.
310-
fn clip_simple_subpaths(subpath1: &Subpath<PointId>, subpath2: &Subpath<PointId>) -> Option<(Subpath<PointId>, Subpath<PointId>)> {
311+
pub fn clip_simple_subpaths(subpath1: &Subpath<PointId>, subpath2: &Subpath<PointId>) -> Option<(Subpath<PointId>, Subpath<PointId>)> {
311312
// Split the first subpath at its last intersection
312313
let intersections1 = subpath1.subpath_intersections(subpath2, None, None);
313314
if intersections1.is_empty() {
@@ -366,6 +367,7 @@ impl<PointId: crate::Identifier> Subpath<PointId> {
366367
.map(|bezier| bezier.offset(distance))
367368
.filter(|subpath| subpath.len() >= 2) // In some cases the reduced and scaled bézier is marked by is_point (so the subpath is empty).
368369
.collect::<Vec<Subpath<PointId>>>();
370+
369371
let mut drop_common_point = vec![true; self.len()];
370372

371373
// Clip or join consecutive Subpaths
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
mod instance;
22
mod merge_by_distance;
3+
pub mod offset_subpath;
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
use crate::vector::PointId;
2+
use bezier_rs::{Bezier, BezierHandles, Join, Subpath, TValue};
3+
4+
/// Value to control smoothness and mathematical accuracy to offset a cubic Bezier.
5+
const CUBIC_REGULARIZATION_ACCURACY: f64 = 0.5;
6+
/// Accuracy of fitting offset curve to Bezier paths.
7+
const CUBIC_TO_BEZPATH_ACCURACY: f64 = 1e-3;
8+
/// Constant used to determine if `f64`s are equivalent.
9+
pub const MAX_ABSOLUTE_DIFFERENCE: f64 = 1e-3;
10+
11+
fn segment_to_bezier(seg: kurbo::PathSeg) -> bezier_rs::Bezier {
12+
match seg {
13+
kurbo::PathSeg::Line(line) => Bezier::from_linear_coordinates(line.p0.x, line.p0.y, line.p1.x, line.p1.y),
14+
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),
15+
kurbo::PathSeg::Cubic(cubic_bez) => Bezier::from_cubic_coordinates(
16+
cubic_bez.p0.x,
17+
cubic_bez.p0.y,
18+
cubic_bez.p1.x,
19+
cubic_bez.p1.y,
20+
cubic_bez.p2.x,
21+
cubic_bez.p2.y,
22+
cubic_bez.p3.x,
23+
cubic_bez.p3.y,
24+
),
25+
}
26+
}
27+
28+
// TODO: Replace the implementation to use only Kurbo API.
29+
/// Reduces the segments of the subpath into simple subcurves, then offset each subcurve a set `distance` away.
30+
/// The intersections of segments of the subpath are joined using the method specified by the `join` argument.
31+
pub fn offset_subpath(subpath: &Subpath<PointId>, distance: f64, join: Join) -> Subpath<PointId> {
32+
// An offset at a distance 0 from the curve is simply the same curve.
33+
// An offset of a single point is not defined.
34+
if distance == 0. || subpath.len() <= 1 || subpath.len_segments() < 1 {
35+
return subpath.clone();
36+
}
37+
38+
let mut subpaths = subpath
39+
.iter()
40+
.filter(|bezier| !bezier.is_point())
41+
.map(|bezier| bezier.to_cubic())
42+
.map(|cubic| {
43+
let Bezier { start, end, handles } = cubic;
44+
let BezierHandles::Cubic { handle_start, handle_end } = handles else { unreachable!()};
45+
46+
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));
47+
let cubic_offset = kurbo::offset::CubicOffset::new_regularized(cubic_bez, distance, CUBIC_REGULARIZATION_ACCURACY);
48+
let offset_bezpath = kurbo::fit_to_bezpath(&cubic_offset, CUBIC_TO_BEZPATH_ACCURACY);
49+
50+
let beziers = offset_bezpath.segments().fold(Vec::new(), |mut acc, seg| {
51+
acc.push(segment_to_bezier(seg));
52+
acc
53+
});
54+
55+
Subpath::from_beziers(&beziers, false)
56+
})
57+
.filter(|subpath| subpath.len() >= 2) // In some cases the reduced and scaled bézier is marked by is_point (so the subpath is empty).
58+
.collect::<Vec<Subpath<PointId>>>();
59+
60+
let mut drop_common_point = vec![true; subpath.len()];
61+
62+
// Clip or join consecutive Subpaths
63+
for i in 0..subpaths.len() - 1 {
64+
let j = i + 1;
65+
let subpath1 = &subpaths[i];
66+
let subpath2 = &subpaths[j];
67+
68+
let last_segment = subpath1.get_segment(subpath1.len_segments() - 1).unwrap();
69+
let first_segment = subpath2.get_segment(0).unwrap();
70+
71+
// If the anchors are approximately equal, there is no need to clip / join the segments
72+
if last_segment.end().abs_diff_eq(first_segment.start(), MAX_ABSOLUTE_DIFFERENCE) {
73+
continue;
74+
}
75+
76+
// Calculate the angle formed between two consecutive Subpaths
77+
let out_tangent = subpath.get_segment(i).unwrap().tangent(TValue::Parametric(1.));
78+
let in_tangent = subpath.get_segment(j).unwrap().tangent(TValue::Parametric(0.));
79+
let angle = out_tangent.angle_to(in_tangent);
80+
81+
// The angle is concave. The Subpath overlap and must be clipped
82+
let mut apply_join = true;
83+
if (angle > 0. && distance > 0.) || (angle < 0. && distance < 0.) {
84+
// If the distance is large enough, there may still be no intersections. Also, if the angle is close enough to zero,
85+
// subpath intersections may find no intersections. In this case, the points are likely close enough that we can approximate
86+
// the points as being on top of one another.
87+
if let Some((clipped_subpath1, clipped_subpath2)) = Subpath::clip_simple_subpaths(subpath1, subpath2) {
88+
subpaths[i] = clipped_subpath1;
89+
subpaths[j] = clipped_subpath2;
90+
apply_join = false;
91+
}
92+
}
93+
// The angle is convex. The Subpath must be joined using the specified join type
94+
if apply_join {
95+
drop_common_point[j] = false;
96+
match join {
97+
Join::Bevel => {}
98+
Join::Miter(miter_limit) => {
99+
let miter_manipulator_group = subpaths[i].miter_line_join(&subpaths[j], miter_limit);
100+
if let Some(miter_manipulator_group) = miter_manipulator_group {
101+
subpaths[i].manipulator_groups_mut().push(miter_manipulator_group);
102+
}
103+
}
104+
Join::Round => {
105+
let (out_handle, round_point, in_handle) = subpaths[i].round_line_join(&subpaths[j], subpath.manipulator_groups()[j].anchor);
106+
let last_index = subpaths[i].manipulator_groups().len() - 1;
107+
subpaths[i].manipulator_groups_mut()[last_index].out_handle = Some(out_handle);
108+
subpaths[i].manipulator_groups_mut().push(round_point);
109+
subpaths[j].manipulator_groups_mut()[0].in_handle = Some(in_handle);
110+
}
111+
}
112+
}
113+
}
114+
115+
// Clip any overlap in the last segment
116+
if subpath.closed {
117+
let out_tangent = subpath.get_segment(subpath.len_segments() - 1).unwrap().tangent(TValue::Parametric(1.));
118+
let in_tangent = subpath.get_segment(0).unwrap().tangent(TValue::Parametric(0.));
119+
let angle = out_tangent.angle_to(in_tangent);
120+
121+
let mut apply_join = true;
122+
if (angle > 0. && distance > 0.) || (angle < 0. && distance < 0.) {
123+
if let Some((clipped_subpath1, clipped_subpath2)) = Subpath::clip_simple_subpaths(&subpaths[subpaths.len() - 1], &subpaths[0]) {
124+
// Merge the clipped subpaths
125+
let last_index = subpaths.len() - 1;
126+
subpaths[last_index] = clipped_subpath1;
127+
subpaths[0] = clipped_subpath2;
128+
apply_join = false;
129+
}
130+
}
131+
if apply_join {
132+
drop_common_point[0] = false;
133+
match join {
134+
Join::Bevel => {}
135+
Join::Miter(miter_limit) => {
136+
let last_subpath_index = subpaths.len() - 1;
137+
let miter_manipulator_group = subpaths[last_subpath_index].miter_line_join(&subpaths[0], miter_limit);
138+
if let Some(miter_manipulator_group) = miter_manipulator_group {
139+
subpaths[last_subpath_index].manipulator_groups_mut().push(miter_manipulator_group);
140+
}
141+
}
142+
Join::Round => {
143+
let last_subpath_index = subpaths.len() - 1;
144+
let (out_handle, round_point, in_handle) = subpaths[last_subpath_index].round_line_join(&subpaths[0], subpath.manipulator_groups()[0].anchor);
145+
let last_index = subpaths[last_subpath_index].manipulator_groups().len() - 1;
146+
subpaths[last_subpath_index].manipulator_groups_mut()[last_index].out_handle = Some(out_handle);
147+
subpaths[last_subpath_index].manipulator_groups_mut().push(round_point);
148+
subpaths[0].manipulator_groups_mut()[0].in_handle = Some(in_handle);
149+
}
150+
}
151+
}
152+
}
153+
154+
// Merge the subpaths. Drop points which overlap with one another.
155+
let mut manipulator_groups = subpaths[0].manipulator_groups().to_vec();
156+
for i in 1..subpaths.len() {
157+
if drop_common_point[i] {
158+
let last_group = manipulator_groups.pop().unwrap();
159+
let mut manipulators_copy = subpaths[i].manipulator_groups().to_vec();
160+
manipulators_copy[0].in_handle = last_group.in_handle;
161+
162+
manipulator_groups.append(&mut manipulators_copy);
163+
} else {
164+
manipulator_groups.append(&mut subpaths[i].manipulator_groups().to_vec());
165+
}
166+
}
167+
if subpath.closed && drop_common_point[0] {
168+
let last_group = manipulator_groups.pop().unwrap();
169+
manipulator_groups[0].in_handle = last_group.in_handle;
170+
}
171+
172+
Subpath::new(manipulator_groups, subpath.closed)
173+
}

node-graph/gcore/src/vector/vector_nodes.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use super::algorithms::offset_subpath::offset_subpath;
12
use super::misc::CentroidType;
23
use super::style::{Fill, Gradient, GradientStops, Stroke};
34
use super::{PointId, SegmentDomain, SegmentId, StrokeId, VectorData, VectorDataTable};
@@ -993,7 +994,8 @@ async fn offset_path(_: impl Ctx, vector_data: VectorDataTable, distance: f64, l
993994
subpath.apply_transform(vector_data_transform);
994995

995996
// Taking the existing stroke data and passing it to Bezier-rs to generate new paths.
996-
let mut subpath_out = subpath.offset(
997+
let mut subpath_out = offset_subpath(
998+
&subpath,
997999
-distance,
9981000
match line_join {
9991001
LineJoin::Miter => Join::Miter(Some(miter_limit)),

0 commit comments

Comments
 (0)