Skip to content

Commit 4696004

Browse files
indierustyKeavon
andcommitted
Refactor the 'Spline' node to use Kurbo instead of Bezier-rs (#2701)
* copy and refactor spline algorithm from Bezier-rs * add comments * refactor spline node * fix spline when path is closed * use refactored version of spline algorithm * revert changes to 'Spline' node funtion * use manipulator groups instead of bezpath * Fix comment --------- Co-authored-by: Keavon Chambers <[email protected]>
1 parent 4dfeafd commit 4696004

File tree

4 files changed

+195
-9
lines changed

4 files changed

+195
-9
lines changed

node-graph/gcore/src/vector/algorithms/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ mod instance;
33
mod merge_by_distance;
44
pub mod offset_subpath;
55
mod poisson_disk;
6+
pub mod spline;
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
use glam::DVec2;
2+
3+
/// Solve for the first handle of an open spline. (The opposite handle can be found by mirroring the result about the anchor.)
4+
pub fn solve_spline_first_handle_open(points: &[DVec2]) -> Vec<DVec2> {
5+
let len_points = points.len();
6+
if len_points == 0 {
7+
return Vec::new();
8+
}
9+
if len_points == 1 {
10+
return vec![points[0]];
11+
}
12+
13+
// Matrix coefficients a, b and c (see https://mathworld.wolfram.com/CubicSpline.html).
14+
// Because the `a` coefficients are all 1, they need not be stored.
15+
// This algorithm does a variation of the above algorithm.
16+
// Instead of using the traditional cubic (a + bt + ct^2 + dt^3), we use the bezier cubic.
17+
18+
let mut b = vec![DVec2::new(4., 4.); len_points];
19+
b[0] = DVec2::new(2., 2.);
20+
b[len_points - 1] = DVec2::new(2., 2.);
21+
22+
let mut c = vec![DVec2::new(1., 1.); len_points];
23+
24+
// 'd' is the the second point in a cubic bezier, which is what we solve for
25+
let mut d = vec![DVec2::ZERO; len_points];
26+
27+
d[0] = DVec2::new(2. * points[1].x + points[0].x, 2. * points[1].y + points[0].y);
28+
d[len_points - 1] = DVec2::new(3. * points[len_points - 1].x, 3. * points[len_points - 1].y);
29+
for idx in 1..(len_points - 1) {
30+
d[idx] = DVec2::new(4. * points[idx].x + 2. * points[idx + 1].x, 4. * points[idx].y + 2. * points[idx + 1].y);
31+
}
32+
33+
// Solve with Thomas algorithm (see https://en.wikipedia.org/wiki/Tridiagonal_matrix_algorithm)
34+
// Now we do row operations to eliminate `a` coefficients.
35+
c[0] /= -b[0];
36+
d[0] /= -b[0];
37+
#[allow(clippy::assign_op_pattern)]
38+
for i in 1..len_points {
39+
b[i] += c[i - 1];
40+
// For some reason this `+=` version makes the borrow checker mad:
41+
// d[i] += d[i-1]
42+
d[i] = d[i] + d[i - 1];
43+
c[i] /= -b[i];
44+
d[i] /= -b[i];
45+
}
46+
47+
// At this point b[i] == -a[i + 1] and a[i] == 0.
48+
// Now we do row operations to eliminate 'c' coefficients and solve.
49+
d[len_points - 1] *= -1.;
50+
#[allow(clippy::assign_op_pattern)]
51+
for i in (0..len_points - 1).rev() {
52+
d[i] = d[i] - (c[i] * d[i + 1]);
53+
d[i] *= -1.; // d[i] /= b[i]
54+
}
55+
56+
d
57+
}
58+
59+
/// Solve for the first handle of a closed spline. (The opposite handle can be found by mirroring the result about the anchor.)
60+
/// If called with fewer than 3 points, this function will return an empty result.
61+
pub fn solve_spline_first_handle_closed(points: &[DVec2]) -> Vec<DVec2> {
62+
let len_points = points.len();
63+
if len_points < 3 {
64+
return Vec::new();
65+
}
66+
67+
// Matrix coefficients `a`, `b` and `c` (see https://mathworld.wolfram.com/CubicSpline.html).
68+
// We don't really need to allocate them but it keeps the maths understandable.
69+
let a = vec![DVec2::ONE; len_points];
70+
let b = vec![DVec2::splat(4.); len_points];
71+
let c = vec![DVec2::ONE; len_points];
72+
73+
let mut cmod = vec![DVec2::ZERO; len_points];
74+
let mut u = vec![DVec2::ZERO; len_points];
75+
76+
// `x` is initially the output of the matrix multiplication, but is converted to the second value.
77+
let mut x = vec![DVec2::ZERO; len_points];
78+
79+
for (i, point) in x.iter_mut().enumerate() {
80+
let previous_i = i.checked_sub(1).unwrap_or(len_points - 1);
81+
let next_i = (i + 1) % len_points;
82+
*point = 3. * (points[next_i] - points[previous_i]);
83+
}
84+
85+
// Solve using https://en.wikipedia.org/wiki/Tridiagonal_matrix_algorithm#Variants (the variant using periodic boundary conditions).
86+
// This code below is based on the reference C language implementation provided in that section of the article.
87+
let alpha = a[0];
88+
let beta = c[len_points - 1];
89+
90+
// Arbitrary, but chosen such that division by zero is avoided.
91+
let gamma = -b[0];
92+
93+
cmod[0] = alpha / (b[0] - gamma);
94+
u[0] = gamma / (b[0] - gamma);
95+
x[0] /= b[0] - gamma;
96+
97+
// Handle from from `1` to `len_points - 2` (inclusive).
98+
for ix in 1..=(len_points - 2) {
99+
let m = 1.0 / (b[ix] - a[ix] * cmod[ix - 1]);
100+
cmod[ix] = c[ix] * m;
101+
u[ix] = (0.0 - a[ix] * u[ix - 1]) * m;
102+
x[ix] = (x[ix] - a[ix] * x[ix - 1]) * m;
103+
}
104+
105+
// Handle `len_points - 1`.
106+
let m = 1.0 / (b[len_points - 1] - alpha * beta / gamma - beta * cmod[len_points - 2]);
107+
u[len_points - 1] = (alpha - a[len_points - 1] * u[len_points - 2]) * m;
108+
x[len_points - 1] = (x[len_points - 1] - a[len_points - 1] * x[len_points - 2]) * m;
109+
110+
// Loop from `len_points - 2` to `0` (inclusive).
111+
for ix in (0..=(len_points - 2)).rev() {
112+
u[ix] = u[ix] - cmod[ix] * u[ix + 1];
113+
x[ix] = x[ix] - cmod[ix] * x[ix + 1];
114+
}
115+
116+
let fact = (x[0] + x[len_points - 1] * beta / gamma) / (1.0 + u[0] + u[len_points - 1] * beta / gamma);
117+
118+
for ix in 0..(len_points) {
119+
x[ix] -= fact * u[ix];
120+
}
121+
122+
let mut real = vec![DVec2::ZERO; len_points];
123+
for i in 0..len_points {
124+
let previous = i.checked_sub(1).unwrap_or(len_points - 1);
125+
let next = (i + 1) % len_points;
126+
real[i] = x[previous] * a[next] + x[i] * b[i] + x[next] * c[i];
127+
}
128+
129+
// The matrix is now solved.
130+
131+
// Since we have computed the derivative, work back to find the start handle.
132+
for i in 0..len_points {
133+
x[i] = (x[i] / 3.) + points[i];
134+
}
135+
136+
x
137+
}
138+
139+
#[test]
140+
fn closed_spline() {
141+
use crate::vector::misc::{dvec2_to_point, point_to_dvec2};
142+
use kurbo::{BezPath, ParamCurve, ParamCurveDeriv};
143+
144+
// These points are just chosen arbitrary
145+
let points = [DVec2::new(0., 0.), DVec2::new(0., 0.), DVec2::new(6., 5.), DVec2::new(7., 9.), DVec2::new(2., 3.)];
146+
147+
// List of first handle or second point in a cubic bezier curve.
148+
let first_handles = solve_spline_first_handle_closed(&points);
149+
150+
// Construct the Subpath
151+
let mut bezpath = BezPath::new();
152+
bezpath.move_to(dvec2_to_point(points[0]));
153+
154+
for i in 0..first_handles.len() {
155+
let next_i = i + 1;
156+
let next_i = if next_i == first_handles.len() { 0 } else { next_i };
157+
158+
// First handle or second point of a cubic Bezier curve.
159+
let p1 = dvec2_to_point(first_handles[i]);
160+
// Second handle or third point of a cubic Bezier curve.
161+
let p2 = dvec2_to_point(2. * points[next_i] - first_handles[next_i]);
162+
// Endpoint or fourth point of a cubic Bezier curve.
163+
let p3 = dvec2_to_point(points[next_i]);
164+
165+
bezpath.curve_to(p1, p2, p3);
166+
}
167+
168+
// For each pair of bézier curves, ensure that the second derivative is continuous
169+
for (bézier_a, bézier_b) in bezpath.segments().zip(bezpath.segments().skip(1).chain(bezpath.segments().take(1))) {
170+
let derivative2_end_a = point_to_dvec2(bézier_a.to_cubic().deriv().eval(1.));
171+
let derivative2_start_b = point_to_dvec2(bézier_b.to_cubic().deriv().eval(0.));
172+
173+
assert!(
174+
derivative2_end_a.abs_diff_eq(derivative2_start_b, 1e-10),
175+
"second derivative at the end of a {derivative2_end_a} is equal to the second derivative at the start of b {derivative2_start_b}"
176+
);
177+
}
178+
}

node-graph/gcore/src/vector/vector_data/attributes.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use crate::vector::misc::dvec2_to_point;
22
use crate::vector::vector_data::{HandleId, VectorData};
3-
use bezier_rs::BezierHandles;
3+
use bezier_rs::{BezierHandles, ManipulatorGroup};
44
use core::iter::zip;
55
use dyn_any::DynAny;
66
use glam::{DAffine2, DVec2};
@@ -849,7 +849,7 @@ impl VectorData {
849849
})
850850
}
851851

852-
fn build_stroke_path_iter(&self) -> StrokePathIter {
852+
pub fn build_stroke_path_iter(&self) -> StrokePathIter {
853853
let mut points = vec![StrokePathIterPointMetadata::default(); self.point_domain.ids().len()];
854854
for (segment_index, (&start, &end)) in self.segment_domain.start_point.iter().zip(&self.segment_domain.end_point).enumerate() {
855855
points[start].set(StrokePathIterPointSegmentMetadata::new(segment_index, false));
@@ -869,6 +869,12 @@ impl VectorData {
869869
self.build_stroke_path_iter().map(|(group, closed)| bezier_rs::Subpath::new(group, closed))
870870
}
871871

872+
/// Construct and return an iterator of Vec of `(bezier_rs::ManipulatorGroup<PointId>], bool)` for stroke.
873+
/// The boolean in the tuple indicates if the path is closed.
874+
pub fn stroke_manipulator_groups(&self) -> impl Iterator<Item = (Vec<ManipulatorGroup<PointId>>, bool)> {
875+
self.build_stroke_path_iter()
876+
}
877+
872878
/// Construct a [`kurbo::BezPath`] curve for stroke.
873879
pub fn stroke_bezpath_iter(&self) -> impl Iterator<Item = kurbo::BezPath> {
874880
self.build_stroke_path_iter().map(|(group, closed)| {

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

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use super::algorithms::bezpath_algorithms::{self, position_on_bezpath, sample_points_on_bezpath, tangent_on_bezpath};
22
use super::algorithms::offset_subpath::offset_subpath;
3+
use super::algorithms::spline::{solve_spline_first_handle_closed, solve_spline_first_handle_open};
34
use super::misc::{CentroidType, point_to_dvec2};
45
use super::style::{Fill, Gradient, GradientStops, Stroke};
56
use super::{PointId, SegmentDomain, SegmentId, StrokeId, VectorData, VectorDataTable};
@@ -1436,15 +1437,15 @@ async fn spline(_: impl Ctx, vector_data: VectorDataTable) -> VectorDataTable {
14361437
}
14371438

14381439
let mut segment_domain = SegmentDomain::default();
1439-
for subpath in vector_data_instance.instance.stroke_bezier_paths() {
1440-
let positions = subpath.manipulator_groups().iter().map(|group| group.anchor).collect::<Vec<_>>();
1441-
let closed = subpath.closed() && positions.len() > 2;
1440+
for (manipulator_groups, closed) in vector_data_instance.instance.stroke_manipulator_groups() {
1441+
let positions = manipulator_groups.iter().map(|group| group.anchor).collect::<Vec<_>>();
1442+
let closed = closed && positions.len() > 2;
14421443

14431444
// Compute control point handles for Bezier spline.
14441445
let first_handles = if closed {
1445-
bezier_rs::solve_spline_first_handle_closed(&positions)
1446+
solve_spline_first_handle_closed(&positions)
14461447
} else {
1447-
bezier_rs::solve_spline_first_handle_open(&positions)
1448+
solve_spline_first_handle_open(&positions)
14481449
};
14491450

14501451
let stroke_id = StrokeId::ZERO;
@@ -1453,8 +1454,8 @@ async fn spline(_: impl Ctx, vector_data: VectorDataTable) -> VectorDataTable {
14531454
for i in 0..(positions.len() - if closed { 0 } else { 1 }) {
14541455
let next_index = (i + 1) % positions.len();
14551456

1456-
let start_index = vector_data_instance.instance.point_domain.resolve_id(subpath.manipulator_groups()[i].id).unwrap();
1457-
let end_index = vector_data_instance.instance.point_domain.resolve_id(subpath.manipulator_groups()[next_index].id).unwrap();
1457+
let start_index = vector_data_instance.instance.point_domain.resolve_id(manipulator_groups[i].id).unwrap();
1458+
let end_index = vector_data_instance.instance.point_domain.resolve_id(manipulator_groups[next_index].id).unwrap();
14581459

14591460
let handle_start = first_handles[i];
14601461
let handle_end = positions[next_index] * 2. - first_handles[next_index];

0 commit comments

Comments
 (0)