Skip to content

Commit 1427fb9

Browse files
indierustyindierustyKeavon
authored
Refactor the 'Sample Points' node to use Kurbo instead of Bezier-rs (#2629)
* fix naming * refactor sample_points node/function. * avoid recalculating segments length to find point on bezpath. * cleanup * rename few variables * fix transformation and use precomputed segment lengths * write comments * set POSITION_ACCURACY and PERIMETER_ACCURACY to smaller value to get better approximate euclidean position on a path * fix segment index when t value is 1.0 * Improve comments * move sampling points code into a separate function * it works! finding the segment index is linear now! * small fix and improve variable names & comment * Naming * evaluate segment at t as euclidean distance. fix. * improve comment & variable name --------- Co-authored-by: indierusty <priyaayadav@gmail.com> Co-authored-by: Keavon Chambers <keavon@keavon.com>
1 parent 9ef9b20 commit 1427fb9

File tree

2 files changed

+127
-152
lines changed

2 files changed

+127
-152
lines changed

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

Lines changed: 95 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
/// Accuracy to find the position on [kurbo::Bezpath].
2-
const POSITION_ACCURACY: f64 = 1e-3;
2+
const POSITION_ACCURACY: f64 = 1e-5;
33
/// Accuracy to find the length of the [kurbo::PathSeg].
4-
const PERIMETER_ACCURACY: f64 = 1e-3;
4+
pub const PERIMETER_ACCURACY: f64 = 1e-5;
55

66
use kurbo::{BezPath, ParamCurve, ParamCurveDeriv, PathSeg, Point, Shape};
77

8-
pub fn position_on_bezpath(bezpath: &BezPath, t: f64, euclidian: bool) -> Point {
9-
let (segment_index, t) = tvalue_to_parametric(bezpath, t, euclidian);
8+
pub fn position_on_bezpath(bezpath: &BezPath, t: f64, euclidian: bool, segments_length: Option<&[f64]>) -> Point {
9+
let (segment_index, t) = t_value_to_parametric(bezpath, t, euclidian, segments_length);
1010
bezpath.get_seg(segment_index + 1).unwrap().eval(t)
1111
}
1212

13-
pub fn tangent_on_bezpath(bezpath: &BezPath, t: f64, euclidian: bool) -> Point {
14-
let (segment_index, t) = tvalue_to_parametric(bezpath, t, euclidian);
13+
pub fn tangent_on_bezpath(bezpath: &BezPath, t: f64, euclidian: bool, segments_length: Option<&[f64]>) -> Point {
14+
let (segment_index, t) = t_value_to_parametric(bezpath, t, euclidian, segments_length);
1515
let segment = bezpath.get_seg(segment_index + 1).unwrap();
1616
match segment {
1717
PathSeg::Line(line) => line.deriv().eval(t),
@@ -20,23 +20,92 @@ pub fn tangent_on_bezpath(bezpath: &BezPath, t: f64, euclidian: bool) -> Point {
2020
}
2121
}
2222

23-
pub fn tvalue_to_parametric(bezpath: &BezPath, t: f64, euclidian: bool) -> (usize, f64) {
23+
pub fn sample_points_on_bezpath(bezpath: BezPath, spacing: f64, start_offset: f64, stop_offset: f64, adaptive_spacing: bool, segments_length: &[f64]) -> Option<BezPath> {
24+
let mut sample_bezpath = BezPath::new();
25+
26+
// Calculate the total length of the collected segments.
27+
let total_length: f64 = segments_length.iter().sum();
28+
29+
// Adjust the usable length by subtracting start and stop offsets.
30+
let mut used_length = total_length - start_offset - stop_offset;
31+
32+
if used_length <= 0. {
33+
return None;
34+
}
35+
36+
// Determine the number of points to generate along the path.
37+
let sample_count = if adaptive_spacing {
38+
// Calculate point count to evenly distribute points while covering the entire path.
39+
// With adaptive spacing, we widen or narrow the points as necessary to ensure the last point is always at the end of the path.
40+
(used_length / spacing).round()
41+
} else {
42+
// Calculate point count based on exact spacing, which may not cover the entire path.
43+
44+
// Without adaptive spacing, we just evenly space the points at the exact specified spacing, usually falling short before the end of the path.
45+
let count = (used_length / spacing + f64::EPSILON).floor();
46+
used_length -= used_length % spacing;
47+
count
48+
};
49+
50+
// Skip if there are no points to generate.
51+
if sample_count < 1. {
52+
return None;
53+
}
54+
55+
// Generate points along the path based on calculated intervals.
56+
let mut length_up_to_previous_segment = 0.;
57+
let mut next_segment_index = 0;
58+
59+
for count in 0..=sample_count as usize {
60+
let fraction = count as f64 / sample_count;
61+
let length_up_to_next_sample_point = fraction * used_length + start_offset;
62+
let mut next_length = length_up_to_next_sample_point - length_up_to_previous_segment;
63+
let mut next_segment_length = segments_length[next_segment_index];
64+
65+
// Keep moving to the next segment while the length up to the next sample point is less or equals to the length up to the segment.
66+
while next_length > next_segment_length {
67+
if next_segment_index == segments_length.len() - 1 {
68+
break;
69+
}
70+
length_up_to_previous_segment += next_segment_length;
71+
next_length = length_up_to_next_sample_point - length_up_to_previous_segment;
72+
next_segment_index += 1;
73+
next_segment_length = segments_length[next_segment_index];
74+
}
75+
76+
let t = (next_length / next_segment_length).clamp(0., 1.);
77+
78+
let segment = bezpath.get_seg(next_segment_index + 1).unwrap();
79+
let t = eval_pathseg_euclidean(segment, t, POSITION_ACCURACY);
80+
let point = segment.eval(t);
81+
82+
if sample_bezpath.elements().is_empty() {
83+
sample_bezpath.move_to(point)
84+
} else {
85+
sample_bezpath.line_to(point)
86+
}
87+
}
88+
89+
Some(sample_bezpath)
90+
}
91+
92+
pub fn t_value_to_parametric(bezpath: &BezPath, t: f64, euclidian: bool, segments_length: Option<&[f64]>) -> (usize, f64) {
2493
if euclidian {
25-
let (segment_index, t) = t_value_to_parametric(bezpath, BezPathTValue::GlobalEuclidean(t));
94+
let (segment_index, t) = bezpath_t_value_to_parametric(bezpath, BezPathTValue::GlobalEuclidean(t), segments_length);
2695
let segment = bezpath.get_seg(segment_index + 1).unwrap();
27-
return (segment_index, eval_pathseg_euclidian(segment, t, POSITION_ACCURACY));
96+
return (segment_index, eval_pathseg_euclidean(segment, t, POSITION_ACCURACY));
2897
}
29-
t_value_to_parametric(bezpath, BezPathTValue::GlobalParametric(t))
98+
bezpath_t_value_to_parametric(bezpath, BezPathTValue::GlobalParametric(t), segments_length)
3099
}
31100

32101
/// Finds the t value of point on the given path segment i.e fractional distance along the segment's total length.
33-
/// It uses a binary search to find the value `t` such that the ratio `length_upto_t / total_length` approximates the input `distance`.
34-
fn eval_pathseg_euclidian(path: kurbo::PathSeg, distance: f64, accuracy: f64) -> f64 {
102+
/// It uses a binary search to find the value `t` such that the ratio `length_up_to_t / total_length` approximates the input `distance`.
103+
pub fn eval_pathseg_euclidean(path_segment: kurbo::PathSeg, distance: f64, accuracy: f64) -> f64 {
35104
let mut low_t = 0.;
36105
let mut mid_t = 0.5;
37106
let mut high_t = 1.;
38107

39-
let total_length = path.perimeter(accuracy);
108+
let total_length = path_segment.perimeter(accuracy);
40109

41110
if !total_length.is_finite() || total_length <= f64::EPSILON {
42111
return 0.;
@@ -45,7 +114,7 @@ fn eval_pathseg_euclidian(path: kurbo::PathSeg, distance: f64, accuracy: f64) ->
45114
let distance = distance.clamp(0., 1.);
46115

47116
while high_t - low_t > accuracy {
48-
let current_length = path.subsegment(0.0..mid_t).perimeter(accuracy);
117+
let current_length = path_segment.subsegment(0.0..mid_t).perimeter(accuracy);
49118
let current_distance = current_length / total_length;
50119

51120
if current_distance > distance {
@@ -71,7 +140,7 @@ fn global_euclidean_to_local_euclidean(bezpath: &kurbo::BezPath, global_t: f64,
71140
}
72141
accumulator += length_ratio;
73142
}
74-
(bezpath.segments().count() - 2, 1.)
143+
(bezpath.segments().count() - 1, 1.)
75144
}
76145

77146
enum BezPathTValue {
@@ -81,24 +150,28 @@ enum BezPathTValue {
81150

82151
/// Convert a [BezPathTValue] to a parametric `(segment_index, t)` tuple.
83152
/// - Asserts that `t` values contained within the `SubpathTValue` argument lie in the range [0, 1].
84-
fn t_value_to_parametric(bezpath: &kurbo::BezPath, t: BezPathTValue) -> (usize, f64) {
85-
let segment_len = bezpath.segments().count();
86-
assert!(segment_len >= 1);
153+
fn bezpath_t_value_to_parametric(bezpath: &kurbo::BezPath, t: BezPathTValue, segments_length: Option<&[f64]>) -> (usize, f64) {
154+
let segment_count = bezpath.segments().count();
155+
assert!(segment_count >= 1);
87156

88157
match t {
89158
BezPathTValue::GlobalEuclidean(t) => {
90-
let lengths = bezpath.segments().map(|bezier| bezier.perimeter(PERIMETER_ACCURACY)).collect::<Vec<f64>>();
91-
let total_length: f64 = lengths.iter().sum();
159+
let lengths = segments_length
160+
.map(|segments_length| segments_length.to_vec())
161+
.unwrap_or(bezpath.segments().map(|segment| segment.perimeter(PERIMETER_ACCURACY)).collect());
162+
163+
let total_length = lengths.iter().sum();
164+
92165
global_euclidean_to_local_euclidean(bezpath, t, lengths.as_slice(), total_length)
93166
}
94167
BezPathTValue::GlobalParametric(global_t) => {
95168
assert!((0.0..=1.).contains(&global_t));
96169

97170
if global_t == 1. {
98-
return (segment_len - 1, 1.);
171+
return (segment_count - 1, 1.);
99172
}
100173

101-
let scaled_t = global_t * segment_len as f64;
174+
let scaled_t = global_t * segment_count as f64;
102175
let segment_index = scaled_t.floor() as usize;
103176
let t = scaled_t - segment_index as f64;
104177

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

Lines changed: 32 additions & 130 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use super::algorithms::bezpath_algorithms::{position_on_bezpath, tangent_on_bezpath};
1+
use super::algorithms::bezpath_algorithms::{PERIMETER_ACCURACY, position_on_bezpath, sample_points_on_bezpath, tangent_on_bezpath};
22
use super::algorithms::offset_subpath::offset_subpath;
33
use super::misc::{CentroidType, point_to_dvec2};
44
use super::style::{Fill, Gradient, GradientStops, Stroke};
@@ -11,11 +11,11 @@ use crate::transform::{Footprint, ReferencePoint, Transform, TransformMut};
1111
use crate::vector::PointDomain;
1212
use crate::vector::style::{LineCap, LineJoin};
1313
use crate::{CloneVarArgs, Color, Context, Ctx, ExtractAll, GraphicElement, GraphicGroupTable, OwnedContextImpl};
14-
use bezier_rs::{Join, ManipulatorGroup, Subpath, SubpathTValue, TValue};
14+
use bezier_rs::{Join, ManipulatorGroup, Subpath, SubpathTValue};
1515
use core::f64::consts::PI;
1616
use core::hash::{Hash, Hasher};
1717
use glam::{DAffine2, DVec2};
18-
use kurbo::Affine;
18+
use kurbo::{Affine, Shape};
1919
use rand::{Rng, SeedableRng};
2020
use std::collections::hash_map::DefaultHasher;
2121

@@ -1147,144 +1147,43 @@ async fn sample_points(_: impl Ctx, vector_data: VectorDataTable, spacing: f64,
11471147
let spacing = spacing.max(0.01);
11481148

11491149
let vector_data_transform = vector_data.transform();
1150-
let vector_data = vector_data.one_instance_ref().instance;
11511150

1152-
// Create an iterator over the bezier segments with enumeration and peeking capability.
1153-
let mut bezier = vector_data.segment_bezier_iter().enumerate().peekable();
1151+
// Using `stroke_bezpath_iter` so that the `subpath_segment_lengths` is aligned to the segments of each bezpath.
1152+
// So we can index into `subpath_segment_lengths` to get the length of the segments.
1153+
// NOTE: `subpath_segment_lengths` has precalulated lengths with transformation applied.
1154+
let bezpaths = vector_data.one_instance_ref().instance.stroke_bezpath_iter();
11541155

11551156
// Initialize the result VectorData with the same transformation as the input.
11561157
let mut result = VectorDataTable::default();
11571158
*result.transform_mut() = vector_data_transform;
11581159

1159-
// Iterate over each segment in the bezier iterator.
1160-
while let Some((index, (segment_id, _, start_point_index, mut last_end))) = bezier.next() {
1161-
// Record the start point index of the subpath.
1162-
let subpath_start_point_index = start_point_index;
1163-
1164-
// Collect connected segments that form a continuous path.
1165-
let mut lengths = vec![(segment_id, subpath_segment_lengths.get(index).copied().unwrap_or_default())];
1166-
1167-
// Continue collecting segments as long as they are connected end-to-start.
1168-
while let Some(&seg) = bezier.peek() {
1169-
let (_, (_, _, ref start, _)) = seg;
1170-
if *start == last_end {
1171-
// Consume the next element since it continues the path.
1172-
let (index, (next_segment_id, _, _, end)) = bezier.next().unwrap();
1173-
last_end = end;
1174-
lengths.push((next_segment_id, subpath_segment_lengths.get(index).copied().unwrap_or_default()));
1175-
} else {
1176-
// The next segment does not continue the path.
1177-
break;
1178-
}
1179-
}
1180-
1181-
// Determine if the subpath is closed.
1182-
let subpath_is_closed = last_end == subpath_start_point_index;
1160+
// Keeps track of the index of the first segment of the next bezpath in order to get lengths of all segments.
1161+
let mut next_segment_index = 0;
11831162

1184-
// Calculate the total length of the collected segments.
1185-
let total_length: f64 = lengths.iter().map(|(_, len)| *len).sum();
1163+
for mut bezpath in bezpaths {
1164+
// Apply the tranformation to the current bezpath to calculate points after transformation.
1165+
bezpath.apply_affine(Affine::new(vector_data_transform.to_cols_array()));
11861166

1187-
// Adjust the usable length by subtracting start and stop offsets.
1188-
let mut used_length = total_length - start_offset - stop_offset;
1189-
if used_length <= 0. {
1190-
continue;
1191-
}
1167+
let segment_count = bezpath.segments().count();
11921168

1193-
// Determine the number of points to generate along the path.
1194-
let count = if adaptive_spacing {
1195-
// Calculate point count to evenly distribute points while covering the entire path.
1196-
// With adaptive spacing, we widen or narrow the points as necessary to ensure the last point is always at the end of the path.
1197-
(used_length / spacing).round()
1198-
} else {
1199-
// Calculate point count based on exact spacing, which may not cover the entire path.
1169+
// For the current bezpath we get its segment's length by calculating the start index and end index.
1170+
let current_bezpath_segments_length = &subpath_segment_lengths[next_segment_index..next_segment_index + segment_count];
12001171

1201-
// Without adaptive spacing, we just evenly space the points at the exact specified spacing, usually falling short before the end of the path.
1202-
let c = (used_length / spacing + f64::EPSILON).floor();
1203-
used_length -= used_length % spacing;
1204-
c
1205-
};
1172+
// Increment the segment index by the number of segments in the current bezpath to calculate the next bezpath segment's length.
1173+
next_segment_index += segment_count;
12061174

1207-
// Skip if there are no points to generate.
1208-
if count < 1. {
1175+
let Some(mut sample_bezpath) = sample_points_on_bezpath(bezpath, spacing, start_offset, stop_offset, adaptive_spacing, current_bezpath_segments_length) else {
12091176
continue;
1210-
}
1211-
1212-
// Initialize a vector to store indices of generated points.
1213-
let mut point_indices = Vec::new();
1214-
1215-
// Generate points along the path based on calculated intervals.
1216-
let max_c = if subpath_is_closed { count as usize - 1 } else { count as usize };
1217-
for c in 0..=max_c {
1218-
let fraction = c as f64 / count;
1219-
let total_distance = fraction * used_length + start_offset;
1220-
1221-
// Find the segment corresponding to the current total_distance.
1222-
let (mut current_segment_id, mut length) = lengths[0];
1223-
let mut total_length_before = 0.;
1224-
for &(next_segment_id, next_length) in lengths.iter().skip(1) {
1225-
if total_length_before + length > total_distance {
1226-
break;
1227-
}
1228-
1229-
total_length_before += length;
1230-
current_segment_id = next_segment_id;
1231-
length = next_length;
1232-
}
1233-
1234-
// Retrieve the segment and apply transformation.
1235-
let Some(segment) = vector_data.segment_from_id(current_segment_id) else { continue };
1236-
let segment = segment.apply_transformation(|point| vector_data_transform.transform_point2(point));
1237-
1238-
// Calculate the position on the segment.
1239-
let parametric_t = segment.euclidean_to_parametric_with_total_length((total_distance - total_length_before) / length, 0.001, length);
1240-
let point = segment.evaluate(TValue::Parametric(parametric_t));
1241-
1242-
// Generate a new PointId and add the point to result.point_domain.
1243-
let point_id = PointId::generate();
1244-
result.one_instance_mut().instance.point_domain.push(point_id, vector_data_transform.inverse().transform_point2(point));
1245-
1246-
// Store the index of the point.
1247-
let point_index = result.one_instance_mut().instance.point_domain.ids().len() - 1;
1248-
point_indices.push(point_index);
1249-
}
1250-
1251-
// After generating points, create segments between consecutive points.
1252-
for window in point_indices.windows(2) {
1253-
if let [start_index, end_index] = *window {
1254-
// Generate a new SegmentId.
1255-
let segment_id = SegmentId::generate();
1256-
1257-
// Use BezierHandles::Linear for linear segments.
1258-
let handles = bezier_rs::BezierHandles::Linear;
1259-
1260-
// Generate a new StrokeId.
1261-
let stroke_id = StrokeId::generate();
1262-
1263-
// Add the segment to result.segment_domain.
1264-
result.one_instance_mut().instance.segment_domain.push(segment_id, start_index, end_index, handles, stroke_id);
1265-
}
1266-
}
1267-
1268-
// If the subpath is closed, add a closing segment connecting the last point to the first point.
1269-
if subpath_is_closed {
1270-
if let (Some(&first_index), Some(&last_index)) = (point_indices.first(), point_indices.last()) {
1271-
// Generate a new SegmentId.
1272-
let segment_id = SegmentId::generate();
1273-
1274-
// Use BezierHandles::Linear for linear segments.
1275-
let handles = bezier_rs::BezierHandles::Linear;
1177+
};
12761178

1277-
// Generate a new StrokeId.
1278-
let stroke_id = StrokeId::generate();
1179+
// Reverse the transformation applied to the bezpath as the `result` already has the transformation set.
1180+
sample_bezpath.apply_affine(Affine::new(vector_data_transform.to_cols_array()).inverse());
12791181

1280-
// Add the closing segment to result.segment_domain.
1281-
result.one_instance_mut().instance.segment_domain.push(segment_id, last_index, first_index, handles, stroke_id);
1282-
}
1283-
}
1182+
// Append the bezpath (subpath) that connects generated points by lines.
1183+
result.one_instance_mut().instance.append_bezpath(sample_bezpath);
12841184
}
1285-
12861185
// Transfer the style from the input vector data to the result.
1287-
result.one_instance_mut().instance.style = vector_data.style.clone();
1186+
result.one_instance_mut().instance.style = vector_data.one_instance_ref().instance.style.clone();
12881187
result.one_instance_mut().instance.style.set_stroke_transform(vector_data_transform);
12891188

12901189
// Return the resulting vector data with newly generated points and segments.
@@ -1320,7 +1219,7 @@ async fn position_on_path(
13201219
let t = if progress == bezpath_count { 1. } else { progress.fract() };
13211220
bezpath.apply_affine(Affine::new(vector_data_transform.to_cols_array()));
13221221

1323-
point_to_dvec2(position_on_bezpath(bezpath, t, euclidian))
1222+
point_to_dvec2(position_on_bezpath(bezpath, t, euclidian, None))
13241223
})
13251224
}
13261225

@@ -1353,10 +1252,10 @@ async fn tangent_on_path(
13531252
let t = if progress == bezpath_count { 1. } else { progress.fract() };
13541253
bezpath.apply_affine(Affine::new(vector_data_transform.to_cols_array()));
13551254

1356-
let mut tangent = point_to_dvec2(tangent_on_bezpath(bezpath, t, euclidian));
1255+
let mut tangent = point_to_dvec2(tangent_on_bezpath(bezpath, t, euclidian, None));
13571256
if tangent == DVec2::ZERO {
13581257
let t = t + if t > 0.5 { -0.001 } else { 0.001 };
1359-
tangent = point_to_dvec2(tangent_on_bezpath(bezpath, t, euclidian));
1258+
tangent = point_to_dvec2(tangent_on_bezpath(bezpath, t, euclidian, None));
13601259
}
13611260
if tangent == DVec2::ZERO {
13621261
return 0.;
@@ -1430,8 +1329,11 @@ async fn subpath_segment_lengths(_: impl Ctx, vector_data: VectorDataTable) -> V
14301329
let vector_data = vector_data.one_instance_ref().instance;
14311330

14321331
vector_data
1433-
.segment_bezier_iter()
1434-
.map(|(_id, bezier, _, _)| bezier.apply_transformation(|point| vector_data_transform.transform_point2(point)).length(None))
1332+
.stroke_bezpath_iter()
1333+
.flat_map(|mut bezpath| {
1334+
bezpath.apply_affine(Affine::new(vector_data_transform.to_cols_array()));
1335+
bezpath.segments().map(|segment| segment.perimeter(PERIMETER_ACCURACY)).collect::<Vec<f64>>()
1336+
})
14351337
.collect()
14361338
}
14371339

0 commit comments

Comments
 (0)