Skip to content

Commit 037bcb6

Browse files
indierustyKeavon
andauthored
Refactor the 'Round Corners' and 'Auto-Tangents' nodes and vector node unit tests to use Kurbo (#2964)
* add todo * impl function to convert a bezpath to manipulator groups * refactor few node impls * refactor vector nodes test and few methods on VectorData struct * refactor tests * remove unused import * simplify and fix morph node test * rename vars and comment * refactor bezpath_to_parametric function * Code review * fix bezpath_to_manipulator_groups function --------- Co-authored-by: Keavon Chambers <[email protected]>
1 parent 668acd3 commit 037bcb6

File tree

5 files changed

+336
-203
lines changed

5 files changed

+336
-203
lines changed

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

Lines changed: 42 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -56,25 +56,26 @@ pub fn split_bezpath_at_segment(bezpath: &BezPath, segment_index: usize, t: f64)
5656
}
5757

5858
/// Splits the [`BezPath`] at a `t` value which lies in the range of [0, 1].
59-
/// Returns [`None`] if the given [`BezPath`] has no segments or `t` is within f64::EPSILON of 0 or 1.
60-
pub fn split_bezpath(bezpath: &BezPath, t: f64, euclidian: bool) -> Option<(BezPath, BezPath)> {
61-
if t <= f64::EPSILON || (1. - t) <= f64::EPSILON || bezpath.segments().count() == 0 {
59+
/// Returns [`None`] if the given [`BezPath`] has no segments.
60+
pub fn split_bezpath(bezpath: &BezPath, t_value: TValue) -> Option<(BezPath, BezPath)> {
61+
if bezpath.segments().count() == 0 {
6262
return None;
6363
}
6464

6565
// Get the segment which lies at the split.
66-
let (segment_index, t) = t_value_to_parametric(bezpath, t, euclidian, None);
66+
let (segment_index, t) = eval_bezpath(bezpath, t_value, None);
6767
split_bezpath_at_segment(bezpath, segment_index, t)
6868
}
6969

70-
pub fn evaluate_bezpath(bezpath: &BezPath, t: f64, euclidian: bool, segments_length: Option<&[f64]>) -> Point {
71-
let (segment_index, t) = t_value_to_parametric(bezpath, t, euclidian, segments_length);
70+
pub fn evaluate_bezpath(bezpath: &BezPath, t_value: TValue, segments_length: Option<&[f64]>) -> Point {
71+
let (segment_index, t) = eval_bezpath(bezpath, t_value, segments_length);
7272
bezpath.get_seg(segment_index + 1).unwrap().eval(t)
7373
}
7474

75-
pub fn tangent_on_bezpath(bezpath: &BezPath, t: f64, euclidian: bool, segments_length: Option<&[f64]>) -> Point {
76-
let (segment_index, t) = t_value_to_parametric(bezpath, t, euclidian, segments_length);
75+
pub fn tangent_on_bezpath(bezpath: &BezPath, t_value: TValue, segments_length: Option<&[f64]>) -> Point {
76+
let (segment_index, t) = eval_bezpath(bezpath, t_value, segments_length);
7777
let segment = bezpath.get_seg(segment_index + 1).unwrap();
78+
7879
match segment {
7980
PathSeg::Line(line) => line.deriv().eval(t),
8081
PathSeg::Quad(quad_bez) => quad_bez.deriv().eval(t),
@@ -180,23 +181,35 @@ pub fn sample_polyline_on_bezpath(
180181
Some(sample_bezpath)
181182
}
182183

183-
pub fn t_value_to_parametric(bezpath: &BezPath, t: f64, euclidian: bool, segments_length: Option<&[f64]>) -> (usize, f64) {
184-
if euclidian {
185-
let (segment_index, t) = bezpath_t_value_to_parametric(bezpath, BezPathTValue::GlobalEuclidean(t), segments_length);
186-
let segment = bezpath.get_seg(segment_index + 1).unwrap();
187-
return (segment_index, eval_pathseg_euclidean(segment, t, DEFAULT_ACCURACY));
184+
#[derive(Debug, Clone, Copy)]
185+
pub enum TValue {
186+
Parametric(f64),
187+
Euclidean(f64),
188+
}
189+
190+
/// Return the subsegment for the given [TValue] range. Returns None if parametric value of `t1` is greater than `t2`.
191+
pub fn trim_pathseg(segment: PathSeg, t1: TValue, t2: TValue) -> Option<PathSeg> {
192+
let t1 = eval_pathseg(segment, t1);
193+
let t2 = eval_pathseg(segment, t2);
194+
195+
if t1 > t2 { None } else { Some(segment.subsegment(t1..t2)) }
196+
}
197+
198+
pub fn eval_pathseg(segment: PathSeg, t_value: TValue) -> f64 {
199+
match t_value {
200+
TValue::Parametric(t) => t,
201+
TValue::Euclidean(t) => eval_pathseg_euclidean(segment, t, DEFAULT_ACCURACY),
188202
}
189-
bezpath_t_value_to_parametric(bezpath, BezPathTValue::GlobalParametric(t), segments_length)
190203
}
191204

192205
/// Finds the t value of point on the given path segment i.e fractional distance along the segment's total length.
193206
/// It uses a binary search to find the value `t` such that the ratio `length_up_to_t / total_length` approximates the input `distance`.
194-
pub fn eval_pathseg_euclidean(path_segment: PathSeg, distance: f64, accuracy: f64) -> f64 {
207+
pub fn eval_pathseg_euclidean(segment: PathSeg, distance: f64, accuracy: f64) -> f64 {
195208
let mut low_t = 0.;
196209
let mut mid_t = 0.5;
197210
let mut high_t = 1.;
198211

199-
let total_length = path_segment.perimeter(accuracy);
212+
let total_length = segment.perimeter(accuracy);
200213

201214
if !total_length.is_finite() || total_length <= f64::EPSILON {
202215
return 0.;
@@ -205,7 +218,7 @@ pub fn eval_pathseg_euclidean(path_segment: PathSeg, distance: f64, accuracy: f6
205218
let distance = distance.clamp(0., 1.);
206219

207220
while high_t - low_t > accuracy {
208-
let current_length = path_segment.subsegment(0.0..mid_t).perimeter(accuracy);
221+
let current_length = segment.subsegment(0.0..mid_t).perimeter(accuracy);
209222
let current_distance = current_length / total_length;
210223

211224
if current_distance > distance {
@@ -222,7 +235,7 @@ pub fn eval_pathseg_euclidean(path_segment: PathSeg, distance: f64, accuracy: f6
222235
/// Converts from a bezpath (composed of multiple segments) to a point along a certain segment represented.
223236
/// The returned tuple represents the segment index and the `t` value along that segment.
224237
/// Both the input global `t` value and the output `t` value are in euclidean space, meaning there is a constant rate of change along the arc length.
225-
fn global_euclidean_to_local_euclidean(bezpath: &BezPath, global_t: f64, lengths: &[f64], total_length: f64) -> (usize, f64) {
238+
fn eval_bazpath_to_euclidean(bezpath: &BezPath, global_t: f64, lengths: &[f64], total_length: f64) -> (usize, f64) {
226239
let mut accumulator = 0.;
227240
for (index, length) in lengths.iter().enumerate() {
228241
let length_ratio = length / total_length;
@@ -234,19 +247,14 @@ fn global_euclidean_to_local_euclidean(bezpath: &BezPath, global_t: f64, lengths
234247
(bezpath.segments().count() - 1, 1.)
235248
}
236249

237-
enum BezPathTValue {
238-
GlobalEuclidean(f64),
239-
GlobalParametric(f64),
240-
}
241-
242-
/// Convert a [BezPathTValue] to a parametric `(segment_index, t)` tuple.
243-
/// - Asserts that `t` values contained within the `SubpathTValue` argument lie in the range [0, 1].
244-
fn bezpath_t_value_to_parametric(bezpath: &BezPath, t: BezPathTValue, precomputed_segments_length: Option<&[f64]>) -> (usize, f64) {
250+
/// Convert a [TValue] to a parametric `(segment_index, t)` tuple.
251+
/// - Asserts that `t` values contained within the `TValue` argument lie in the range [0, 1].
252+
fn eval_bezpath(bezpath: &BezPath, t: TValue, precomputed_segments_length: Option<&[f64]>) -> (usize, f64) {
245253
let segment_count = bezpath.segments().count();
246254
assert!(segment_count >= 1);
247255

248256
match t {
249-
BezPathTValue::GlobalEuclidean(t) => {
257+
TValue::Euclidean(t) => {
250258
let computed_segments_length;
251259

252260
let segments_length = if let Some(segments_length) = precomputed_segments_length {
@@ -258,16 +266,18 @@ fn bezpath_t_value_to_parametric(bezpath: &BezPath, t: BezPathTValue, precompute
258266

259267
let total_length = segments_length.iter().sum();
260268

261-
global_euclidean_to_local_euclidean(bezpath, t, segments_length, total_length)
269+
let (segment_index, t) = eval_bazpath_to_euclidean(bezpath, t, segments_length, total_length);
270+
let segment = bezpath.get_seg(segment_index + 1).unwrap();
271+
(segment_index, eval_pathseg_euclidean(segment, t, DEFAULT_ACCURACY))
262272
}
263-
BezPathTValue::GlobalParametric(global_t) => {
264-
assert!((0.0..=1.).contains(&global_t));
273+
TValue::Parametric(t) => {
274+
assert!((0.0..=1.).contains(&t));
265275

266-
if global_t == 1. {
276+
if t == 1. {
267277
return (segment_count - 1, 1.);
268278
}
269279

270-
let scaled_t = global_t * segment_count as f64;
280+
let scaled_t = t * segment_count as f64;
271281
let segment_index = scaled_t.floor() as usize;
272282
let t = scaled_t - segment_index as f64;
273283

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

Lines changed: 71 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1+
use super::PointId;
2+
use super::algorithms::offset_subpath::MAX_ABSOLUTE_DIFFERENCE;
13
use bezier_rs::{BezierHandles, ManipulatorGroup, Subpath};
24
use dyn_any::DynAny;
35
use glam::DVec2;
4-
use kurbo::{BezPath, CubicBez, Line, PathSeg, Point, QuadBez};
5-
6-
use super::PointId;
6+
use kurbo::{BezPath, CubicBez, Line, ParamCurve, PathSeg, Point, QuadBez};
7+
use std::ops::Sub;
78

89
/// Represents different ways of calculating the centroid.
910
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type, node_macro::ChoiceType)]
@@ -169,3 +170,70 @@ pub fn bezpath_from_manipulator_groups(manipulator_groups: &[ManipulatorGroup<Po
169170
}
170171
bezpath
171172
}
173+
174+
pub fn bezpath_to_manipulator_groups(bezpath: &BezPath) -> (Vec<ManipulatorGroup<PointId>>, bool) {
175+
let mut manipulator_groups = Vec::<ManipulatorGroup<PointId>>::new();
176+
let mut is_closed = false;
177+
178+
for element in bezpath.elements() {
179+
let manipulator_group = match *element {
180+
kurbo::PathEl::MoveTo(point) => ManipulatorGroup::new(point_to_dvec2(point), None, None),
181+
kurbo::PathEl::LineTo(point) => ManipulatorGroup::new(point_to_dvec2(point), None, None),
182+
kurbo::PathEl::QuadTo(point, point1) => ManipulatorGroup::new(point_to_dvec2(point1), Some(point_to_dvec2(point)), None),
183+
kurbo::PathEl::CurveTo(point, point1, point2) => {
184+
if let Some(last_maipulator_group) = manipulator_groups.last_mut() {
185+
last_maipulator_group.out_handle = Some(point_to_dvec2(point));
186+
}
187+
ManipulatorGroup::new(point_to_dvec2(point2), Some(point_to_dvec2(point1)), None)
188+
}
189+
kurbo::PathEl::ClosePath => {
190+
if let Some(last_group) = manipulator_groups.pop() {
191+
if let Some(first_group) = manipulator_groups.first_mut() {
192+
first_group.out_handle = last_group.in_handle;
193+
}
194+
}
195+
is_closed = true;
196+
break;
197+
}
198+
};
199+
200+
manipulator_groups.push(manipulator_group);
201+
}
202+
203+
(manipulator_groups, is_closed)
204+
}
205+
206+
/// Returns true if the [`PathSeg`] is equivalent to a line.
207+
///
208+
/// This is different from simply checking if the segment is [`PathSeg::Line`] or [`PathSeg::Quad`] or [`PathSeg::Cubic`]. Bezier curve can also be a line if the control points are colinear to the start and end points. Therefore if the handles exceed the start and end point, it will still be considered as a line.
209+
pub fn is_linear(segment: PathSeg) -> bool {
210+
let is_colinear = |a: Point, b: Point, c: Point| -> bool { ((b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x)).abs() < MAX_ABSOLUTE_DIFFERENCE };
211+
212+
match segment {
213+
PathSeg::Line(_) => true,
214+
PathSeg::Quad(QuadBez { p0, p1, p2 }) => is_colinear(p0, p1, p2),
215+
PathSeg::Cubic(CubicBez { p0, p1, p2, p3 }) => is_colinear(p0, p1, p3) && is_colinear(p0, p2, p3),
216+
}
217+
}
218+
219+
/// Get an iterator over the coordinates of all points in a path segment.
220+
pub fn get_segment_points(segment: PathSeg) -> Vec<Point> {
221+
match segment {
222+
PathSeg::Line(line) => [line.p0, line.p1].to_vec(),
223+
PathSeg::Quad(quad_bez) => [quad_bez.p0, quad_bez.p1, quad_bez.p2].to_vec(),
224+
PathSeg::Cubic(cubic_bez) => [cubic_bez.p0, cubic_bez.p1, cubic_bez.p2, cubic_bez.p3].to_vec(),
225+
}
226+
}
227+
228+
/// Returns true if the corresponding points of the two [`PathSeg`]s are within the provided absolute value difference from each other.
229+
pub fn pathseg_abs_diff_eq(seg1: PathSeg, seg2: PathSeg, max_abs_diff: f64) -> bool {
230+
let seg1 = if is_linear(seg1) { PathSeg::Line(Line::new(seg1.start(), seg1.end())) } else { seg1 };
231+
let seg2 = if is_linear(seg2) { PathSeg::Line(Line::new(seg2.start(), seg2.end())) } else { seg2 };
232+
233+
let seg1_points = get_segment_points(seg1);
234+
let seg2_points = get_segment_points(seg2);
235+
236+
let cmp = |a: f64, b: f64| a.sub(b).abs() < max_abs_diff;
237+
238+
seg1_points.len() == seg2_points.len() && seg1_points.into_iter().zip(seg2_points).all(|(a, b)| cmp(a.x, b.x) && cmp(a.y, b.y))
239+
}

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ use core::hash::Hash;
1717
use dyn_any::DynAny;
1818
use glam::{DAffine2, DVec2};
1919
pub use indexed::VectorDataIndex;
20-
use kurbo::{Affine, Rect, Shape};
20+
use kurbo::{Affine, BezPath, Rect, Shape};
2121
pub use modification::*;
2222
use std::collections::HashMap;
2323

@@ -195,6 +195,13 @@ impl VectorData {
195195
Self::from_subpaths([subpath], false)
196196
}
197197

198+
/// Construct some new vector data from a single [`BezPath`] with an identity transform and black fill.
199+
pub fn from_bezpath(bezpath: BezPath) -> Self {
200+
let mut vector_data = Self::default();
201+
vector_data.append_bezpath(bezpath);
202+
vector_data
203+
}
204+
198205
/// Construct some new vector data from subpaths with an identity transform and black fill.
199206
pub fn from_subpaths(subpaths: impl IntoIterator<Item = impl Borrow<bezier_rs::Subpath<PointId>>>, preserve_id: bool) -> Self {
200207
let mut vector_data = Self::default();

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

Lines changed: 50 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use crate::vector::vector_data::{HandleId, VectorData};
33
use bezier_rs::{BezierHandles, ManipulatorGroup};
44
use dyn_any::DynAny;
55
use glam::{DAffine2, DVec2};
6+
use kurbo::{CubicBez, Line, PathSeg, QuadBez};
67
use std::collections::HashMap;
78
use std::hash::{Hash, Hasher};
89
use std::iter::zip;
@@ -673,6 +674,18 @@ impl FoundSubpath {
673674
}
674675

675676
impl VectorData {
677+
/// Construct a [`kurbo::PathSeg`] by resolving the points from their ids.
678+
fn path_segment_from_index(&self, start: usize, end: usize, handles: BezierHandles) -> PathSeg {
679+
let start = dvec2_to_point(self.point_domain.positions()[start]);
680+
let end = dvec2_to_point(self.point_domain.positions()[end]);
681+
682+
match handles {
683+
BezierHandles::Linear => PathSeg::Line(Line::new(start, end)),
684+
BezierHandles::Quadratic { handle } => PathSeg::Quad(QuadBez::new(start, dvec2_to_point(handle), end)),
685+
BezierHandles::Cubic { handle_start, handle_end } => PathSeg::Cubic(CubicBez::new(start, dvec2_to_point(handle_start), dvec2_to_point(handle_end), end)),
686+
}
687+
}
688+
676689
/// Construct a [`bezier_rs::Bezier`] curve spanning from the resolved position of the start and end points with the specified handles.
677690
fn segment_to_bezier_with_index(&self, start: usize, end: usize, handles: BezierHandles) -> bezier_rs::Bezier {
678691
let start = self.point_domain.positions()[start];
@@ -699,6 +712,19 @@ impl VectorData {
699712
(start_id, end_id, self.segment_to_bezier_with_index(start, end, self.segment_domain.handles[index]))
700713
}
701714

715+
/// Iterator over all of the [`bezier_rs::Bezier`] following the order that they are stored in the segment domain, skipping invalid segments.
716+
pub fn segment_iter(&self) -> impl Iterator<Item = (SegmentId, PathSeg, PointId, PointId)> {
717+
let to_segment = |(((&handles, &id), &start), &end)| (id, self.path_segment_from_index(start, end, handles), self.point_domain.ids()[start], self.point_domain.ids()[end]);
718+
719+
self.segment_domain
720+
.handles
721+
.iter()
722+
.zip(&self.segment_domain.id)
723+
.zip(self.segment_domain.start_point())
724+
.zip(self.segment_domain.end_point())
725+
.map(to_segment)
726+
}
727+
702728
/// Iterator over all of the [`bezier_rs::Bezier`] following the order that they are stored in the segment domain, skipping invalid segments.
703729
pub fn segment_bezier_iter(&self) -> impl Iterator<Item = (SegmentId, bezier_rs::Bezier, PointId, PointId)> + '_ {
704730
let to_bezier = |(((&handles, &id), &start), &end)| (id, self.segment_to_bezier_with_index(start, end, handles), self.point_domain.ids()[start], self.point_domain.ids()[end]);
@@ -819,48 +845,8 @@ impl VectorData {
819845
Some(bezier_rs::Subpath::new(groups, closed))
820846
}
821847

822-
/// Construct a [`bezier_rs::Bezier`] curve from an iterator of segments with (handles, start point, end point). Returns None if any ids are invalid or if the segments are not continuous.
823-
fn subpath_from_segments(&self, segments: impl Iterator<Item = (BezierHandles, usize, usize)>) -> Option<bezier_rs::Subpath<PointId>> {
824-
let mut first_point = None;
825-
let mut groups = Vec::new();
826-
let mut last: Option<(usize, BezierHandles)> = None;
827-
828-
for (handle, start, end) in segments {
829-
if last.is_some_and(|(previous_end, _)| previous_end != start) {
830-
warn!("subpath_from_segments that were not continuous");
831-
return None;
832-
}
833-
first_point = Some(first_point.unwrap_or(start));
834-
835-
groups.push(ManipulatorGroup {
836-
anchor: self.point_domain.positions()[start],
837-
in_handle: last.and_then(|(_, handle)| handle.end()),
838-
out_handle: handle.start(),
839-
id: self.point_domain.ids()[start],
840-
});
841-
842-
last = Some((end, handle));
843-
}
844-
845-
let closed = groups.len() > 1 && last.map(|(point, _)| point) == first_point;
846-
847-
if let Some((end, last_handle)) = last {
848-
if closed {
849-
groups[0].in_handle = last_handle.end();
850-
} else {
851-
groups.push(ManipulatorGroup {
852-
anchor: self.point_domain.positions()[end],
853-
in_handle: last_handle.end(),
854-
out_handle: None,
855-
id: self.point_domain.ids()[end],
856-
});
857-
}
858-
}
859-
Some(bezier_rs::Subpath::new(groups, closed))
860-
}
861-
862848
/// Construct a [`bezier_rs::Bezier`] curve for each region, skipping invalid regions.
863-
pub fn region_bezier_paths(&self) -> impl Iterator<Item = (RegionId, bezier_rs::Subpath<PointId>)> + '_ {
849+
pub fn region_manipulator_groups(&self) -> impl Iterator<Item = (RegionId, Vec<ManipulatorGroup<PointId>>)> + '_ {
864850
self.region_domain
865851
.id
866852
.iter()
@@ -876,7 +862,29 @@ impl VectorData {
876862
.zip(self.segment_domain.end_point.get(range)?)
877863
.map(|((&handles, &start), &end)| (handles, start, end));
878864

879-
self.subpath_from_segments(segments_iter).map(|subpath| (id, subpath))
865+
let mut manipulator_groups = Vec::new();
866+
let mut in_handle = None;
867+
868+
for segment in segments_iter {
869+
let (handles, start_point_index, _end_point_index) = segment;
870+
let start_point_id = self.point_domain.id[start_point_index];
871+
let start_point = self.point_domain.position[start_point_index];
872+
873+
let (manipulator_group, next_in_handle) = match handles {
874+
BezierHandles::Linear => (ManipulatorGroup::new_with_id(start_point, in_handle, None, start_point_id), None),
875+
BezierHandles::Quadratic { handle } => (ManipulatorGroup::new_with_id(start_point, in_handle, Some(handle), start_point_id), None),
876+
BezierHandles::Cubic { handle_start, handle_end } => (ManipulatorGroup::new_with_id(start_point, in_handle, Some(handle_start), start_point_id), Some(handle_end)),
877+
};
878+
879+
in_handle = next_in_handle;
880+
manipulator_groups.push(manipulator_group);
881+
}
882+
883+
if let Some(first) = manipulator_groups.first_mut() {
884+
first.in_handle = in_handle;
885+
}
886+
887+
Some((id, manipulator_groups))
880888
})
881889
}
882890

0 commit comments

Comments
 (0)