diff --git a/libraries/bezier-rs/src/bezier/lookup.rs b/libraries/bezier-rs/src/bezier/lookup.rs
index 5706514a..f4dc8cbc 100644
--- a/libraries/bezier-rs/src/bezier/lookup.rs
+++ b/libraries/bezier-rs/src/bezier/lookup.rs
@@ -6,24 +6,52 @@ use super::*;
impl Bezier {
/// Convert a euclidean distance ratio along the `Bezier` curve to a parametric `t`-value.
pub fn euclidean_to_parametric(&self, ratio: f64, error: f64) -> f64 {
- if ratio < error {
+ let total_length = self.length(None);
+ self.euclidean_to_parametric_with_total_length(ratio, error, total_length)
+ }
+
+ /// Convert a euclidean distance ratio along the `Bezier` curve to a parametric `t`-value.
+ /// For performance reasons, this version of the [`euclidean_to_parametric`] function allows the caller to
+ /// provide the total length of the curve so it doesn't have to be calculated every time the function is called.
+ pub fn euclidean_to_parametric_with_total_length(&self, euclidean_t: f64, error: f64, total_length: f64) -> f64 {
+ if euclidean_t < error {
return 0.;
}
- if 1. - ratio < error {
+ if 1. - euclidean_t < error {
return 1.;
}
let mut low = 0.;
- let mut mid = 0.;
+ let mut mid = 0.5;
let mut high = 1.;
- let total_length = self.length(None);
+ // The euclidean t-value input generally correlates with the parametric t-value result.
+ // So we can assume a low t-value has a short length from the start of the curve, and a high t-value has a short length from the end of the curve.
+ // We'll use a strategy where we measure from either end of the curve depending on which side is closer than thus more likely to be proximate to the sought parametric t-value.
+ // This allows us to use fewer segments to approximate the curve, which usually won't go much beyond half the curve.
+ let result_likely_closer_to_start = euclidean_t < 0.5;
+ // If the curve is near either end, we need even fewer segments to approximate the curve with reasonable accuracy.
+ // A point that's likely near the center is the worst case where we need to use up to half the predefined number of max subdivisions.
+ let subdivisions_proportional_to_likely_length = ((euclidean_t - 0.5).abs() * DEFAULT_LENGTH_SUBDIVISIONS as f64).round().max(1.) as usize;
+
+ // Binary search for the parametric t-value that corresponds to the euclidean distance ratio by trimming the curve between the start and the tested parametric t-value during each iteration of the search.
while low < high {
mid = (low + high) / 2.;
- let test_ratio = self.trim(TValue::Parametric(0.), TValue::Parametric(mid)).length(None) / total_length;
- if f64_compare(test_ratio, ratio, error) {
+
+ // We can search from the curve start to the sought point, or from the sought point to the curve end, depending on which side is likely closer to the result.
+ let current_length = if result_likely_closer_to_start {
+ let trimmed = self.trim(TValue::Parametric(0.), TValue::Parametric(mid));
+ trimmed.length(Some(subdivisions_proportional_to_likely_length))
+ } else {
+ let trimmed = self.trim(TValue::Parametric(mid), TValue::Parametric(1.));
+ let trimmed_length = trimmed.length(Some(subdivisions_proportional_to_likely_length));
+ total_length - trimmed_length
+ };
+ let current_euclidean_t = current_length / total_length;
+
+ if f64_compare(current_euclidean_t, euclidean_t, error) {
break;
- } else if test_ratio < ratio {
+ } else if current_euclidean_t < euclidean_t {
low = mid;
} else {
high = mid;
@@ -101,22 +129,14 @@ impl Bezier {
///
pub fn length(&self, num_subdivisions: Option) -> f64 {
match self.handles {
- BezierHandles::Linear => self.start.distance(self.end),
+ BezierHandles::Linear => (self.start - self.end).length(),
_ => {
// Code example from .
// We will use an approximate approach where we split the curve into many subdivisions
// and calculate the euclidean distance between the two endpoints of the subdivision
let lookup_table = self.compute_lookup_table(Some(num_subdivisions.unwrap_or(DEFAULT_LENGTH_SUBDIVISIONS)), Some(TValueType::Parametric));
- let mut approx_curve_length = 0.;
- let mut previous_point = lookup_table[0];
- // Calculate approximate distance between subdivision
- for current_point in lookup_table.iter().skip(1) {
- // Calculate distance of subdivision
- approx_curve_length += (*current_point - previous_point).length();
- // Update the previous point
- previous_point = *current_point;
- }
+ let approx_curve_length: f64 = lookup_table.windows(2).map(|points| (points[1] - points[0]).length()).sum();
approx_curve_length
}
diff --git a/libraries/bezier-rs/src/subpath/lookup.rs b/libraries/bezier-rs/src/subpath/lookup.rs
index 5bf69c95..c6752644 100644
--- a/libraries/bezier-rs/src/subpath/lookup.rs
+++ b/libraries/bezier-rs/src/subpath/lookup.rs
@@ -28,13 +28,13 @@ impl Subpath {
/// - `num_subdivisions` - Number of subdivisions used to approximate the curve. The default value is `1000`.
///
pub fn length(&self, num_subdivisions: Option) -> f64 {
- self.iter().fold(0., |accumulator, bezier| accumulator + bezier.length(num_subdivisions))
+ self.iter().map(|bezier| bezier.length(num_subdivisions)).sum()
}
- fn global_euclidean_to_local_euclidean(&self, global_t: f64) -> (usize, f64) {
- let lengths = self.iter().map(|bezier| bezier.length(None)).collect::>();
- let total_length: f64 = lengths.iter().sum();
-
+ /// Converts from a subpath (composed of multiple segments) to a point along a certain segment represented.
+ /// The returned tuple represents the segment index and the `t` value along that segment.
+ /// 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.
+ pub fn global_euclidean_to_local_euclidean(&self, global_t: f64, lengths: &[f64], total_length: f64) -> (usize, f64) {
let mut accumulator = 0.;
for (index, length) in lengths.iter().enumerate() {
let length_ratio = length / total_length;
@@ -77,11 +77,11 @@ impl Subpath {
(segment_index, self.get_segment(segment_index).unwrap().euclidean_to_parametric(t, DEFAULT_EUCLIDEAN_ERROR_BOUND))
}
SubpathTValue::GlobalEuclidean(t) => {
- let (segment_index, segment_t) = self.global_euclidean_to_local_euclidean(t);
- (
- segment_index,
- self.get_segment(segment_index).unwrap().euclidean_to_parametric(segment_t, DEFAULT_EUCLIDEAN_ERROR_BOUND),
- )
+ let lengths = self.iter().map(|bezier| bezier.length(None)).collect::>();
+ let total_length: f64 = lengths.iter().sum();
+ let (segment_index, segment_t_euclidean) = self.global_euclidean_to_local_euclidean(t, lengths.as_slice(), total_length);
+ let segment_t_parametric = self.get_segment(segment_index).unwrap().euclidean_to_parametric(segment_t_euclidean, DEFAULT_EUCLIDEAN_ERROR_BOUND);
+ (segment_index, segment_t_parametric)
}
SubpathTValue::EuclideanWithinError { segment_index, t, error } => {
assert!((0.0..=1.).contains(&t));
@@ -89,7 +89,9 @@ impl Subpath {
(segment_index, self.get_segment(segment_index).unwrap().euclidean_to_parametric(t, error))
}
SubpathTValue::GlobalEuclideanWithinError { t, error } => {
- let (segment_index, segment_t) = self.global_euclidean_to_local_euclidean(t);
+ let lengths = self.iter().map(|bezier| bezier.length(None)).collect::>();
+ let total_length: f64 = lengths.iter().sum();
+ let (segment_index, segment_t) = self.global_euclidean_to_local_euclidean(t, lengths.as_slice(), total_length);
(segment_index, self.get_segment(segment_index).unwrap().euclidean_to_parametric(segment_t, error))
}
}
diff --git a/libraries/bezier-rs/src/subpath/solvers.rs b/libraries/bezier-rs/src/subpath/solvers.rs
index 46bafabc..4281993a 100644
--- a/libraries/bezier-rs/src/subpath/solvers.rs
+++ b/libraries/bezier-rs/src/subpath/solvers.rs
@@ -17,7 +17,7 @@ impl Subpath {
/// Calculates the intersection points the subpath has with a given curve and returns a list of `(usize, f64)` tuples,
/// where the `usize` represents the index of the curve in the subpath, and the `f64` represents the `t`-value local to
- /// that curve where the intersection occured.
+ /// that curve where the intersection occurred.
/// Expects the following:
/// - `other`: a [Bezier] curve to check intersections against
/// - `error`: an optional f64 value to provide an error bound
diff --git a/libraries/bezier-rs/src/utils.rs b/libraries/bezier-rs/src/utils.rs
index 2e2e7c6d..d717325a 100644
--- a/libraries/bezier-rs/src/utils.rs
+++ b/libraries/bezier-rs/src/utils.rs
@@ -262,8 +262,8 @@ pub fn compute_circle_center_from_points(p1: DVec2, p2: DVec2, p3: DVec2) -> Opt
}
/// Compare two `f64` numbers with a provided max absolute value difference.
-pub fn f64_compare(f1: f64, f2: f64, max_abs_diff: f64) -> bool {
- (f1 - f2).abs() < max_abs_diff
+pub fn f64_compare(a: f64, b: f64, max_abs_diff: f64) -> bool {
+ (a - b).abs() < max_abs_diff
}
/// Determine if an `f64` number is within a given range by using a max absolute value difference comparison.
@@ -272,8 +272,8 @@ pub fn f64_approximately_in_range(value: f64, min: f64, max: f64, max_abs_diff:
}
/// Compare the two values in a `DVec2` independently with a provided max absolute value difference.
-pub fn dvec2_compare(dv1: DVec2, dv2: DVec2, max_abs_diff: f64) -> BVec2 {
- BVec2::new((dv1.x - dv2.x).abs() < max_abs_diff, (dv1.y - dv2.y).abs() < max_abs_diff)
+pub fn dvec2_compare(a: DVec2, b: DVec2, max_abs_diff: f64) -> BVec2 {
+ BVec2::new((a.x - b.x).abs() < max_abs_diff, (a.y - b.y).abs() < max_abs_diff)
}
/// Determine if the values in a `DVec2` are within a given range independently by using a max absolute value difference comparison.
@@ -323,8 +323,8 @@ mod tests {
use crate::consts::MAX_ABSOLUTE_DIFFERENCE;
/// Compare vectors of `f64`s with a provided max absolute value difference.
- fn f64_compare_vector(vec1: Vec, vec2: Vec, max_abs_diff: f64) -> bool {
- vec1.len() == vec2.len() && vec1.into_iter().zip(vec2).all(|(a, b)| f64_compare(a, b, max_abs_diff))
+ fn f64_compare_vector(a: Vec, b: Vec, max_abs_diff: f64) -> bool {
+ a.len() == b.len() && a.into_iter().zip(b).all(|(a, b)| f64_compare(a, b, max_abs_diff))
}
#[test]
diff --git a/node-graph/gcore/src/vector/vector_nodes.rs b/node-graph/gcore/src/vector/vector_nodes.rs
index 9b7db7e0..9cfb61f1 100644
--- a/node-graph/gcore/src/vector/vector_nodes.rs
+++ b/node-graph/gcore/src/vector/vector_nodes.rs
@@ -5,9 +5,8 @@ use crate::transform::{Footprint, Transform, TransformMut};
use crate::{Color, GraphicGroup, Node};
use core::future::Future;
-use bezier_rs::{Subpath, SubpathTValue};
+use bezier_rs::{Subpath, TValue};
use glam::{DAffine2, DVec2};
-use num_traits::Zero;
#[derive(Debug, Clone, Copy)]
pub struct SetFillNode {
@@ -165,7 +164,7 @@ impl ConcatElement for VectorData {
impl ConcatElement for GraphicGroup {
fn concat(&mut self, other: &Self, transform: DAffine2) {
- // TODO: Decide if we want to keep this behaviour whereby the layers are flattened
+ // TODO: Decide if we want to keep this behavior whereby the layers are flattened
for mut element in other.iter().cloned() {
*element.transform_mut() = transform * element.transform() * other.transform();
self.push(element);
@@ -223,30 +222,40 @@ fn sample_points(mut vector_data: VectorData, spacing: f32, start_offset: f32, s
}
subpath.apply_transform(vector_data.transform);
- let length = subpath.length(None);
- let used_length = length - start_offset - stop_offset;
+
+ let segment_lengths = subpath.iter().map(|bezier| bezier.length(None)).collect::>();
+ let total_length: f64 = segment_lengths.iter().sum();
+
+ let mut used_length = total_length - start_offset - stop_offset;
if used_length <= 0. {
continue;
}
- let used_length_without_remainder = used_length - used_length % spacing;
- let count = if adaptive_spacing {
- (used_length / spacing).round()
+ let count;
+ if adaptive_spacing {
+ count = (used_length / spacing).round();
} else {
- (used_length / spacing + f64::EPSILON).floor()
- };
+ count = (used_length / spacing + f64::EPSILON).floor();
+ used_length = used_length - used_length % spacing;
+ }
if count >= 1. {
let new_anchors = (0..=count as usize).map(|c| {
let ratio = c as f64 / count;
- // With adaptive spacing, we widen or narrow the points (that's the rounding performed above) as necessary to ensure the last point is always at the end of the path
- // Without adaptive spacing, we just evenly space the points at the exact specified spacing, usually falling short before the end of the path
+ // With adaptive spacing, we widen or narrow the points (that's the `round()` above) as necessary to ensure the last point is always at the end of the path.
+ // Without adaptive spacing, we just evenly space the points at the exact specified spacing, usually falling short (that's the `floor()` above) before the end of the path.
- let used_length_here = if adaptive_spacing { used_length } else { used_length_without_remainder };
- let t = ratio * used_length_here + start_offset;
- subpath.evaluate(SubpathTValue::GlobalEuclidean(t / length))
+ let t = (ratio * used_length + start_offset) / total_length;
+
+ let (segment_index, segment_t_euclidean) = subpath.global_euclidean_to_local_euclidean(t, segment_lengths.as_slice(), total_length);
+ let segment_t_parametric = subpath
+ .get_segment(segment_index)
+ .unwrap()
+ .euclidean_to_parametric_with_total_length(segment_t_euclidean, 0.001, segment_lengths[segment_index]);
+ subpath.get_segment(segment_index).unwrap().evaluate(TValue::Parametric(segment_t_parametric))
});
+
*subpath = Subpath::from_anchors(new_anchors, subpath.closed() && count as usize > 1);
}