From 681cf6f5e56ff4287b50b0f474d0899c34539034 Mon Sep 17 00:00:00 2001
From: 0hypercube <0hypercube@gmail.com>
Date: Sat, 17 Feb 2024 20:04:44 +0000
Subject: [PATCH 1/6] Initial vector format structure
---
.../tool/common_functionality/shape_editor.rs | 8 +-
.../snapping/layer_snapper.rs | 2 +-
libraries/bezier-rs/src/bezier/lookup.rs | 268 +++++++-------
.../bezier-rs/src/bezier/manipulators.rs | 12 +-
libraries/bezier-rs/src/bezier/mod.rs | 32 +-
libraries/bezier-rs/src/bezier/structs.rs | 24 --
libraries/bezier-rs/src/bezier/transform.rs | 23 +-
libraries/bezier-rs/src/subpath/core.rs | 101 +++---
libraries/bezier-rs/src/subpath/lookup.rs | 12 +-
libraries/bezier-rs/src/subpath/mod.rs | 1 +
libraries/bezier-rs/src/subpath/transform.rs | 4 +-
libraries/bezier-rs/src/utils.rs | 5 -
libraries/dyn-any/src/lib.rs | 7 +-
.../gcore/src/graphic_element/renderer.rs | 5 +-
node-graph/gcore/src/uuid.rs | 4 +
.../gcore/src/vector/generator_nodes.rs | 8 +-
node-graph/gcore/src/vector/vector_data.rs | 65 +++-
.../src/vector/vector_data/attributes.rs | 330 ++++++++++++++++++
node-graph/gcore/src/vector/vector_nodes.rs | 166 ++++-----
node-graph/gstd/src/brush.rs | 2 +-
.../interpreted-executor/src/node_registry.rs | 2 +-
.../other/bezier-rs-demos/wasm/src/bezier.rs | 4 +-
.../other/bezier-rs-demos/wasm/src/subpath.rs | 2 +-
23 files changed, 702 insertions(+), 385 deletions(-)
create mode 100644 node-graph/gcore/src/vector/vector_data/attributes.rs
diff --git a/editor/src/messages/tool/common_functionality/shape_editor.rs b/editor/src/messages/tool/common_functionality/shape_editor.rs
index eac7fecbd3b..0932faffcfd 100644
--- a/editor/src/messages/tool/common_functionality/shape_editor.rs
+++ b/editor/src/messages/tool/common_functionality/shape_editor.rs
@@ -125,7 +125,7 @@ impl ClosestSegment {
}
fn t_min_max(bezier: &Bezier, layer_scale: DVec2) -> (f64, f64) {
- let length = bezier.apply_transformation(|point| point * layer_scale).length(Some(100));
+ let length = bezier.apply_transformation(|point| point * layer_scale).length(None);
let too_close_t = (INSERT_POINT_ON_SEGMENT_TOO_CLOSE_DISTANCE / length).min(0.5);
let t_min_euclidean = too_close_t;
@@ -148,7 +148,7 @@ impl ClosestSegment {
// Linear approximation of parametric t-value ranges:
let t_min = self.t_min / self.scale;
let t_max = 1. - ((1. - self.t_max) / self.scale);
- let t = self.bezier.project(layer_m_pos, None).max(t_min).min(t_max);
+ let t = self.bezier.project(layer_m_pos).max(t_min).min(t_max);
self.t = t;
let bezier_point = self.bezier.evaluate(TValue::Parametric(t));
@@ -1099,8 +1099,6 @@ impl ShapeState {
let scale = document_metadata.document_to_viewport.decompose_scale().x;
let tolerance = tolerance + 0.5 * scale; // make more talerance at large scale
- let lut_size = ((5. + scale) as usize).min(20); // need more precision at large scale
- let projection_options = bezier_rs::ProjectionOptions { lut_size, ..Default::default() };
let mut closest = None;
let mut closest_distance_squared: f64 = tolerance * tolerance;
@@ -1109,7 +1107,7 @@ impl ShapeState {
for (subpath_index, subpath) in subpaths.iter().enumerate() {
for (manipulator_index, bezier) in subpath.iter().enumerate() {
- let t = bezier.project(layer_pos, Some(projection_options));
+ let t = bezier.project(layer_pos);
let layerspace = bezier.evaluate(TValue::Parametric(t));
let screenspace = transform.transform_point2(layerspace);
diff --git a/editor/src/messages/tool/common_functionality/snapping/layer_snapper.rs b/editor/src/messages/tool/common_functionality/snapping/layer_snapper.rs
index 15da8496c66..623e181685f 100644
--- a/editor/src/messages/tool/common_functionality/snapping/layer_snapper.rs
+++ b/editor/src/messages/tool/common_functionality/snapping/layer_snapper.rs
@@ -94,7 +94,7 @@ impl LayerSnapper {
if path.document_curve.start.distance_squared(path.document_curve.end) < tolerance * tolerance * 2. {
continue;
}
- let time = path.document_curve.project(point.document_point, None);
+ let time = path.document_curve.project(point.document_point);
let snapped_point_document = path.document_curve.evaluate(bezier_rs::TValue::Parametric(time));
let distance = snapped_point_document.distance(point.document_point);
diff --git a/libraries/bezier-rs/src/bezier/lookup.rs b/libraries/bezier-rs/src/bezier/lookup.rs
index f4dc8cbcc01..7f53a97dbd1 100644
--- a/libraries/bezier-rs/src/bezier/lookup.rs
+++ b/libraries/bezier-rs/src/bezier/lookup.rs
@@ -1,4 +1,4 @@
-use crate::utils::{f64_compare, TValue, TValueType};
+use crate::utils::{TValue, TValueType};
use super::*;
@@ -21,44 +21,57 @@ impl Bezier {
return 1.;
}
- let mut low = 0.;
- let mut mid = 0.5;
- let mut high = 1.;
-
- // 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.;
-
- // 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 current_euclidean_t < euclidean_t {
- low = mid;
- } else {
- high = mid;
+ match self.handles {
+ BezierHandles::Linear => euclidean_t,
+ BezierHandles::Quadratic { handle } => {
+ // Use Casteljau subdivision, noting that the length is more than the straight line distance from start to end but less than the straight line distance through the handles
+ fn recurse(a0: DVec2, a1: DVec2, a2: DVec2, level: u8, desired_len: f64) -> (f64, f64) {
+ let lower = a0.distance(a2);
+ let upper = a0.distance(a1) + a1.distance(a2);
+ if level >= 8 {
+ let approx_len = (lower + upper) / 2.;
+ return (approx_len, desired_len / approx_len);
+ }
+
+ let b1 = 0.5 * (a0 + a1);
+ let c1 = 0.5 * (a1 + a2);
+ let b2 = 0.5 * (b1 + c1);
+ let (first_len, t) = recurse(a0, b1, b2, level + 1, desired_len);
+ if first_len > desired_len {
+ return (first_len, t * 0.5);
+ }
+ let (second_len, t) = recurse(b2, c1, a2, level + 1, desired_len - first_len);
+ (first_len + second_len, t * 0.5 + 0.5)
+ }
+ recurse(self.start, handle, self.end, 0, total_length * euclidean_t).1
+ }
+ BezierHandles::Cubic { handle_start, handle_end } => {
+ // Use Casteljau subdivision, noting that the length is more than the straight line distance from start to end but less than the straight line distance through the handles
+ fn recurse(a0: DVec2, a1: DVec2, a2: DVec2, a3: DVec2, level: u8, desired_len: f64) -> (f64, f64) {
+ let lower = a0.distance(a3);
+ let upper = a0.distance(a1) + a1.distance(a2) + a2.distance(a3);
+ if level >= 8 {
+ let approx_len = (lower + upper) / 2.;
+ return (approx_len, desired_len / approx_len);
+ }
+
+ let b1 = 0.5 * (a0 + a1);
+ let t0 = 0.5 * (a1 + a2);
+ let c1 = 0.5 * (a2 + a3);
+ let b2 = 0.5 * (b1 + t0);
+ let c2 = 0.5 * (t0 + c1);
+ let b3 = 0.5 * (b2 + c2);
+ let (first_len, t) = recurse(a0, b1, b2, b3, level + 1, desired_len);
+ if first_len > desired_len {
+ return (first_len, t * 0.5);
+ }
+ let (second_len, t) = recurse(b3, c2, c1, a3, level + 1, desired_len - first_len);
+ (first_len + second_len, t * 0.5 + 0.5)
+ }
+ recurse(self.start, handle_start, handle_end, self.end, 0, total_length * euclidean_t).1
}
}
-
- mid
+ .clamp(0., 1.)
}
/// Convert a [TValue] to a parametric `t`-value.
@@ -109,133 +122,86 @@ impl Bezier {
/// Return a selection of equidistant points on the bezier curve.
/// If no value is provided for `steps`, then the function will default `steps` to be 10.
///
- pub fn compute_lookup_table(&self, steps: Option, tvalue_type: Option) -> Vec {
+ pub fn compute_lookup_table(&self, steps: Option, tvalue_type: Option) -> impl Iterator- + '_ {
let steps = steps.unwrap_or(DEFAULT_LUT_STEP_SIZE);
let tvalue_type = tvalue_type.unwrap_or(TValueType::Parametric);
- (0..=steps)
- .map(|t| {
- let tvalue = match tvalue_type {
- TValueType::Parametric => TValue::Parametric(t as f64 / steps as f64),
- TValueType::Euclidean => TValue::Euclidean(t as f64 / steps as f64),
- };
- self.evaluate(tvalue)
- })
- .collect()
+ (0..=steps).map(move |t| {
+ let tvalue = match tvalue_type {
+ TValueType::Parametric => TValue::Parametric(t as f64 / steps as f64),
+ TValueType::Euclidean => TValue::Euclidean(t as f64 / steps as f64),
+ };
+ self.evaluate(tvalue)
+ })
}
/// Return an approximation of the length of the bezier curve.
- /// - `num_subdivisions` - Number of subdivisions used to approximate the curve. The default value is 1000.
+ /// - `tolerance` - Tolerance used to approximate the curve.
///
- pub fn length(&self, num_subdivisions: Option) -> f64 {
+ pub fn length(&self, tolerance: Option) -> f64 {
match self.handles {
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 approx_curve_length: f64 = lookup_table.windows(2).map(|points| (points[1] - points[0]).length()).sum();
-
- approx_curve_length
+ BezierHandles::Quadratic { handle } => {
+ // Use Casteljau subdivision, noting that the length is more than the straight line distance from start to end but less than the straight line distance through the handles
+ fn recurse(a0: DVec2, a1: DVec2, a2: DVec2, tolerance: f64, level: u8) -> f64 {
+ let lower = a0.distance(a2);
+ let upper = a0.distance(a1) + a1.distance(a2);
+ if upper - lower <= 2. * tolerance || level >= 8 {
+ return (lower + upper) / 2.;
+ }
+
+ let b1 = 0.5 * (a0 + a1);
+ let c1 = 0.5 * (a1 + a2);
+ let b2 = 0.5 * (b1 + c1);
+ recurse(a0, b1, b2, 0.5 * tolerance, level + 1) + recurse(b2, c1, a2, 0.5 * tolerance, level + 1)
+ }
+ recurse(self.start, handle, self.end, tolerance.unwrap_or_default(), 0)
+ }
+ BezierHandles::Cubic { handle_start, handle_end } => {
+ // Use Casteljau subdivision, noting that the length is more than the straight line distance from start to end but less than the straight line distance through the handles
+ fn recurse(a0: DVec2, a1: DVec2, a2: DVec2, a3: DVec2, tolerance: f64, level: u8) -> f64 {
+ let lower = a0.distance(a3);
+ let upper = a0.distance(a1) + a1.distance(a2) + a2.distance(a3);
+ if upper - lower <= 2. * tolerance || level >= 8 {
+ return (lower + upper) / 2.;
+ }
+
+ let b1 = 0.5 * (a0 + a1);
+ let t0 = 0.5 * (a1 + a2);
+ let c1 = 0.5 * (a2 + a3);
+ let b2 = 0.5 * (b1 + t0);
+ let c2 = 0.5 * (t0 + c1);
+ let b3 = 0.5 * (b2 + c2);
+ recurse(a0, b1, b2, b3, 0.5 * tolerance, level + 1) + recurse(b3, c2, c1, a3, 0.5 * tolerance, level + 1)
+ }
+ recurse(self.start, handle_start, handle_end, self.end, tolerance.unwrap_or_default(), 0)
}
}
}
/// Returns the parametric `t`-value that corresponds to the closest point on the curve to the provided point.
- /// Uses a searching algorithm akin to binary search that can be customized using the optional [ProjectionOptions] struct.
///
- pub fn project(&self, point: DVec2, options: Option) -> f64 {
- let options = options.unwrap_or_default();
- let ProjectionOptions {
- lut_size,
- convergence_epsilon,
- convergence_limit,
- iteration_limit,
- } = options;
-
- // TODO: Consider optimizations from precomputing useful values, or using the GPU
- // First find the closest point from the results of a lookup table
- let lut = self.compute_lookup_table(Some(lut_size), Some(TValueType::Parametric));
- let (minimum_position, minimum_distance) = utils::get_closest_point_in_lut(&lut, point);
-
- // Get the t values to the left and right of the closest result in the lookup table
- let lut_size_f64 = lut_size as f64;
- let minimum_position_f64 = minimum_position as f64;
- let mut left_t = (minimum_position_f64 - 1.).max(0.) / lut_size_f64;
- let mut right_t = (minimum_position_f64 + 1.).min(lut_size_f64) / lut_size_f64;
-
- // Perform a finer search by finding closest t from 5 points between [left_t, right_t] inclusive
- // Choose new left_t and right_t for a smaller range around the closest t and repeat the process
- let mut final_t = left_t;
- let mut distance;
-
- // Increment minimum_distance to ensure that the distance < minimum_distance comparison will be true for at least one iteration
- let mut new_minimum_distance = minimum_distance + 1.;
- // Maintain the previous distance to identify convergence
- let mut previous_distance;
- // Counter to limit the number of iterations
- let mut iteration_count = 0;
- // Counter to identify how many iterations have had a similar result. Used for convergence test
- let mut convergence_count = 0;
-
- // Store calculated distances to minimize unnecessary recomputations
- let mut distances: [f64; NUM_DISTANCES] = [
- point.distance(lut[(minimum_position as i64 - 1).max(0) as usize]),
- 0.,
- 0.,
- 0.,
- point.distance(lut[lut_size.min(minimum_position + 1)]),
- ];
-
- while left_t <= right_t && convergence_count < convergence_limit && iteration_count < iteration_limit {
- previous_distance = new_minimum_distance;
- let step = (right_t - left_t) / (NUM_DISTANCES as f64 - 1.);
- let mut iterator_t = left_t;
- let mut target_index = 0;
- // Iterate through first 4 points and will handle the right most point later
- for (step_index, table_distance) in distances.iter_mut().enumerate().take(4) {
- // Use previously computed distance for the left most point, and compute new values for the others
- if step_index == 0 {
- distance = *table_distance;
- } else {
- distance = point.distance(self.evaluate(TValue::Parametric(iterator_t)));
- *table_distance = distance;
- }
- if distance < new_minimum_distance {
- new_minimum_distance = distance;
- target_index = step_index;
- final_t = iterator_t
- }
- iterator_t += step;
- }
- // Check right most edge separately since step may not perfectly add up to it (floating point errors)
- if distances[NUM_DISTANCES - 1] < new_minimum_distance {
- new_minimum_distance = distances[NUM_DISTANCES - 1];
- final_t = right_t;
- }
-
- // Update left_t and right_t to be the t values (final_t +/- step), while handling the edges (i.e. if final_t is 0, left_t will be 0 instead of -step)
- // Ensure that the t values never exceed the [0, 1] range
- left_t = (final_t - step).max(0.);
- right_t = (final_t + step).min(1.);
-
- // Re-use the corresponding computed distances (target_index is the index corresponding to final_t)
- // Since target_index is a u_size, can't subtract one if it is zero
- distances[0] = distances[if target_index == 0 { 0 } else { target_index - 1 }];
- distances[NUM_DISTANCES - 1] = distances[(target_index + 1).min(NUM_DISTANCES - 1)];
-
- iteration_count += 1;
- // update count for consecutive iterations of similar minimum distances
- if previous_distance - new_minimum_distance < convergence_epsilon {
- convergence_count += 1;
- } else {
- convergence_count = 0;
+ pub fn project(&self, point: DVec2) -> f64 {
+ let sbasis = crate::symmetrical_basis::to_symmetrical_basis_pair(*self);
+ let derivative = sbasis.derivative();
+ let dd = (sbasis - point).dot(&derivative);
+ let roots = dd.roots();
+
+ let mut closest = 0.;
+ let mut min_dist_squared = self.evaluate(TValue::Parametric(0.)).distance_squared(point);
+
+ for time in roots {
+ let distance = self.evaluate(TValue::Parametric(time)).distance_squared(point);
+ if distance < min_dist_squared {
+ closest = time;
+ min_dist_squared = distance;
}
}
- final_t
+ if self.evaluate(TValue::Parametric(1.)).distance_squared(point) < min_dist_squared {
+ closest = 1.;
+ }
+ closest
}
}
@@ -259,11 +225,11 @@ mod tests {
#[test]
fn test_compute_lookup_table() {
let bezier1 = Bezier::from_quadratic_coordinates(10., 10., 30., 30., 50., 10.);
- let lookup_table1 = bezier1.compute_lookup_table(Some(2), Some(TValueType::Parametric));
+ let lookup_table1 = bezier1.compute_lookup_table(Some(2), Some(TValueType::Parametric)).collect::>();
assert_eq!(lookup_table1, vec![bezier1.start(), bezier1.evaluate(TValue::Parametric(0.5)), bezier1.end()]);
let bezier2 = Bezier::from_cubic_coordinates(10., 10., 30., 30., 70., 70., 90., 10.);
- let lookup_table2 = bezier2.compute_lookup_table(Some(4), Some(TValueType::Parametric));
+ let lookup_table2 = bezier2.compute_lookup_table(Some(4), Some(TValueType::Parametric)).collect::>();
assert_eq!(
lookup_table2,
vec![
@@ -296,10 +262,10 @@ mod tests {
#[test]
fn test_project() {
let bezier1 = Bezier::from_cubic_coordinates(4., 4., 23., 45., 10., 30., 56., 90.);
- assert_eq!(bezier1.project(DVec2::ZERO, None), 0.);
- assert_eq!(bezier1.project(DVec2::new(100., 100.), None), 1.);
+ assert_eq!(bezier1.project(DVec2::ZERO,), 0.);
+ assert_eq!(bezier1.project(DVec2::new(100., 100.),), 1.);
let bezier2 = Bezier::from_quadratic_coordinates(0., 0., 0., 100., 100., 100.);
- assert_eq!(bezier2.project(DVec2::new(100., 0.), None), 0.);
+ assert_eq!(bezier2.project(DVec2::new(100., 0.),), 0.);
}
}
diff --git a/libraries/bezier-rs/src/bezier/manipulators.rs b/libraries/bezier-rs/src/bezier/manipulators.rs
index 5224783b7a3..e56e8d9218b 100644
--- a/libraries/bezier-rs/src/bezier/manipulators.rs
+++ b/libraries/bezier-rs/src/bezier/manipulators.rs
@@ -57,20 +57,12 @@ impl Bezier {
/// Get the coordinates of the bezier segment's first handle point. This represents the only handle in a quadratic segment.
pub fn handle_start(&self) -> Option {
- match self.handles {
- BezierHandles::Linear => None,
- BezierHandles::Quadratic { handle } => Some(handle),
- BezierHandles::Cubic { handle_start, .. } => Some(handle_start),
- }
+ self.handles.start()
}
/// Get the coordinates of the second handle point. This will return `None` for a quadratic segment.
pub fn handle_end(&self) -> Option {
- match self.handles {
- BezierHandles::Linear { .. } => None,
- BezierHandles::Quadratic { .. } => None,
- BezierHandles::Cubic { handle_end, .. } => Some(handle_end),
- }
+ self.handles.end()
}
/// Get an iterator over the coordinates of all points in a vector.
diff --git a/libraries/bezier-rs/src/bezier/mod.rs b/libraries/bezier-rs/src/bezier/mod.rs
index 5831ee5574a..bf5086b9ecb 100644
--- a/libraries/bezier-rs/src/bezier/mod.rs
+++ b/libraries/bezier-rs/src/bezier/mod.rs
@@ -14,7 +14,7 @@ use glam::DVec2;
use std::fmt::{Debug, Formatter, Result};
/// Representation of the handle point(s) in a bezier segment.
-#[derive(Copy, Clone, PartialEq)]
+#[derive(Copy, Clone, PartialEq, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum BezierHandles {
Linear,
@@ -35,6 +35,36 @@ impl BezierHandles {
pub fn is_cubic(&self) -> bool {
matches!(self, Self::Cubic { .. })
}
+ /// Get the coordinates of the bezier segment's first handle point. This represents the only handle in a quadratic segment.
+ pub fn start(&self) -> Option {
+ match *self {
+ BezierHandles::Cubic { handle_start, .. } | BezierHandles::Quadratic { handle: handle_start } => Some(handle_start),
+ _ => None,
+ }
+ }
+
+ /// Get the coordinates of the second handle point. This will return `None` for a quadratic segment.
+ pub fn end(&self) -> Option {
+ match *self {
+ BezierHandles::Cubic { handle_end, .. } => Some(handle_end),
+ _ => None,
+ }
+ }
+ /// Returns a Bezier curve that results from applying the transformation function to each point in the Bezier.
+ pub fn apply_transformation(&self, transformation_function: impl Fn(DVec2) -> DVec2) -> Self {
+ match *self {
+ BezierHandles::Linear => Self::Linear,
+ BezierHandles::Quadratic { handle } => {
+ let handle = transformation_function(handle);
+ Self::Quadratic { handle }
+ }
+ BezierHandles::Cubic { handle_start, handle_end } => {
+ let handle_start = transformation_function(handle_start);
+ let handle_end = transformation_function(handle_end);
+ Self::Cubic { handle_start, handle_end }
+ }
+ }
+ }
}
#[cfg(feature = "dyn-any")]
diff --git a/libraries/bezier-rs/src/bezier/structs.rs b/libraries/bezier-rs/src/bezier/structs.rs
index 04b9e38f6ff..088d541bef3 100644
--- a/libraries/bezier-rs/src/bezier/structs.rs
+++ b/libraries/bezier-rs/src/bezier/structs.rs
@@ -1,30 +1,6 @@
use glam::DVec2;
use std::fmt::{Debug, Formatter, Result};
-/// Struct to represent optional parameters that can be passed to the `project` function.
-#[derive(Copy, Clone)]
-pub struct ProjectionOptions {
- /// Size of the lookup table for the initial passthrough. The default value is `20`.
- pub lut_size: usize,
- /// Difference used between floating point numbers to be considered as equal. The default value is `0.0001`
- pub convergence_epsilon: f64,
- /// Controls the number of iterations needed to consider that minimum distance to have converged. The default value is `3`.
- pub convergence_limit: usize,
- /// Controls the maximum total number of iterations to be used. The default value is `10`.
- pub iteration_limit: usize,
-}
-
-impl Default for ProjectionOptions {
- fn default() -> Self {
- Self {
- lut_size: 20,
- convergence_epsilon: 1e-4,
- convergence_limit: 3,
- iteration_limit: 10,
- }
- }
-}
-
/// Struct used to represent the different strategies for generating arc approximations.
#[derive(Copy, Clone)]
pub enum ArcStrategy {
diff --git a/libraries/bezier-rs/src/bezier/transform.rs b/libraries/bezier-rs/src/bezier/transform.rs
index 69f45b0ef53..94460e11d9e 100644
--- a/libraries/bezier-rs/src/bezier/transform.rs
+++ b/libraries/bezier-rs/src/bezier/transform.rs
@@ -105,19 +105,10 @@ impl Bezier {
/// Returns a Bezier curve that results from applying the transformation function to each point in the Bezier.
pub fn apply_transformation(&self, transformation_function: impl Fn(DVec2) -> DVec2) -> Bezier {
- let transformed_start = transformation_function(self.start);
- let transformed_end = transformation_function(self.end);
- match self.handles {
- BezierHandles::Linear => Bezier::from_linear_dvec2(transformed_start, transformed_end),
- BezierHandles::Quadratic { handle } => {
- let transformed_handle = transformation_function(handle);
- Bezier::from_quadratic_dvec2(transformed_start, transformed_handle, transformed_end)
- }
- BezierHandles::Cubic { handle_start, handle_end } => {
- let transformed_handle_start = transformation_function(handle_start);
- let transformed_handle_end = transformation_function(handle_end);
- Bezier::from_cubic_dvec2(transformed_start, transformed_handle_start, transformed_handle_end, transformed_end)
- }
+ Self {
+ start: transformation_function(self.start),
+ end: transformation_function(self.end),
+ handles: self.handles.apply_transformation(transformation_function),
}
}
@@ -315,12 +306,12 @@ impl Bezier {
BezierHandles::Linear => Bezier::from_linear_dvec2(transformed_start, transformed_end),
BezierHandles::Quadratic { handle: _ } => unreachable!(),
BezierHandles::Cubic { handle_start, handle_end } => {
- let handle_start_closest_t = intermediate.project(handle_start, None);
+ let handle_start_closest_t = intermediate.project(handle_start);
let handle_start_scale_distance = (1. - handle_start_closest_t) * start_distance + handle_start_closest_t * end_distance;
let transformed_handle_start =
utils::scale_point_from_direction_vector(handle_start, intermediate.normal(TValue::Parametric(handle_start_closest_t)), false, handle_start_scale_distance);
- let handle_end_closest_t = intermediate.project(handle_start, None);
+ let handle_end_closest_t = intermediate.project(handle_start);
let handle_end_scale_distance = (1. - handle_end_closest_t) * start_distance + handle_end_closest_t * end_distance;
let transformed_handle_end = utils::scale_point_from_direction_vector(handle_end, intermediate.normal(TValue::Parametric(handle_end_closest_t)), false, handle_end_scale_distance);
Bezier::from_cubic_dvec2(transformed_start, transformed_handle_start, transformed_handle_end, transformed_end)
@@ -810,7 +801,7 @@ mod tests {
.iter()
.map(|t| {
let offset_point = offset_segment.evaluate(TValue::Parametric(*t));
- let closest_point_t = bezier.project(offset_point, None);
+ let closest_point_t = bezier.project(offset_point);
let closest_point = bezier.evaluate(TValue::Parametric(closest_point_t));
let actual_distance = offset_point.distance(closest_point);
diff --git a/libraries/bezier-rs/src/subpath/core.rs b/libraries/bezier-rs/src/subpath/core.rs
index d63d457ea5a..bd6a9468a09 100644
--- a/libraries/bezier-rs/src/subpath/core.rs
+++ b/libraries/bezier-rs/src/subpath/core.rs
@@ -8,6 +8,7 @@ use std::fmt::Write;
impl Subpath {
/// Create a new `Subpath` using a list of [ManipulatorGroup]s.
/// A `Subpath` with less than 2 [ManipulatorGroup]s may not be closed.
+ #[track_caller]
pub fn new(manipulator_groups: Vec>, closed: bool) -> Self {
assert!(!closed || manipulator_groups.len() > 1, "A closed Subpath must contain more than 1 ManipulatorGroup.");
Self { manipulator_groups, closed }
@@ -274,61 +275,71 @@ impl Subpath {
// Number of points = number of points to find handles for
let len_points = points.len();
- // matrix coefficients a, b and c (see https://mathworld.wolfram.com/CubicSpline.html)
- // because the 'a' coefficients are all 1 they need not be stored
- // this algorithm does a variation of the above algorithm.
- // Instead of using the traditional cubic: a + bt + ct^2 + dt^3, we use the bezier cubic.
-
- let mut b = vec![DVec2::new(4., 4.); len_points];
- b[0] = DVec2::new(2., 2.);
- b[len_points - 1] = DVec2::new(2., 2.);
-
- let mut c = vec![DVec2::new(1., 1.); len_points];
-
- // 'd' is the the second point in a cubic bezier, which is what we solve for
- let mut d = vec![DVec2::ZERO; len_points];
-
- d[0] = DVec2::new(2. * points[1].x + points[0].x, 2. * points[1].y + points[0].y);
- d[len_points - 1] = DVec2::new(3. * points[len_points - 1].x, 3. * points[len_points - 1].y);
- for idx in 1..(len_points - 1) {
- d[idx] = DVec2::new(4. * points[idx].x + 2. * points[idx + 1].x, 4. * points[idx].y + 2. * points[idx + 1].y);
- }
-
- // Solve with Thomas algorithm (see https://en.wikipedia.org/wiki/Tridiagonal_matrix_algorithm)
- // do row operations to eliminate `a` coefficients
- c[0] /= -b[0];
- d[0] /= -b[0];
- #[allow(clippy::assign_op_pattern)]
- for i in 1..len_points {
- b[i] += c[i - 1];
- // for some reason the below line makes the borrow checker mad
- //d[i] += d[i-1]
- d[i] = d[i] + d[i - 1];
- c[i] /= -b[i];
- d[i] /= -b[i];
- }
-
- // at this point b[i] == -a[i + 1], a[i] == 0,
- // do row operations to eliminate 'c' coefficients and solve
- d[len_points - 1] *= -1.;
- #[allow(clippy::assign_op_pattern)]
- for i in (0..len_points - 1).rev() {
- d[i] = d[i] - (c[i] * d[i + 1]);
- d[i] *= -1.; //d[i] /= b[i]
- }
+ let out_handles = solve_spline_first_handle(&points);
let mut subpath = Subpath::new(Vec::new(), false);
// given the second point in the n'th cubic bezier, the third point is given by 2 * points[n+1] - b[n+1].
// to find 'handle1_pos' for the n'th point we need the n-1 cubic bezier
- subpath.manipulator_groups.push(ManipulatorGroup::new(points[0], None, Some(d[0])));
+ subpath.manipulator_groups.push(ManipulatorGroup::new(points[0], None, Some(out_handles[0])));
for i in 1..len_points - 1 {
- subpath.manipulator_groups.push(ManipulatorGroup::new(points[i], Some(2. * points[i] - d[i]), Some(d[i])));
+ subpath
+ .manipulator_groups
+ .push(ManipulatorGroup::new(points[i], Some(2. * points[i] - out_handles[i]), Some(out_handles[i])));
}
subpath
.manipulator_groups
- .push(ManipulatorGroup::new(points[len_points - 1], Some(2. * points[len_points - 1] - d[len_points - 1]), None));
+ .push(ManipulatorGroup::new(points[len_points - 1], Some(2. * points[len_points - 1] - out_handles[len_points - 1]), None));
subpath
}
}
+
+pub fn solve_spline_first_handle(points: &[DVec2]) -> Vec {
+ let len_points = points.len();
+
+ // matrix coefficients a, b and c (see https://mathworld.wolfram.com/CubicSpline.html)
+ // because the 'a' coefficients are all 1 they need not be stored
+ // this algorithm does a variation of the above algorithm.
+ // Instead of using the traditional cubic: a + bt + ct^2 + dt^3, we use the bezier cubic.
+
+ let mut b = vec![DVec2::new(4., 4.); len_points];
+ b[0] = DVec2::new(2., 2.);
+ b[len_points - 1] = DVec2::new(2., 2.);
+
+ let mut c = vec![DVec2::new(1., 1.); len_points];
+
+ // 'd' is the the second point in a cubic bezier, which is what we solve for
+ let mut d = vec![DVec2::ZERO; len_points];
+
+ d[0] = DVec2::new(2. * points[1].x + points[0].x, 2. * points[1].y + points[0].y);
+ d[len_points - 1] = DVec2::new(3. * points[len_points - 1].x, 3. * points[len_points - 1].y);
+ for idx in 1..(len_points - 1) {
+ d[idx] = DVec2::new(4. * points[idx].x + 2. * points[idx + 1].x, 4. * points[idx].y + 2. * points[idx + 1].y);
+ }
+
+ // Solve with Thomas algorithm (see https://en.wikipedia.org/wiki/Tridiagonal_matrix_algorithm)
+ // do row operations to eliminate `a` coefficients
+ c[0] /= -b[0];
+ d[0] /= -b[0];
+ #[allow(clippy::assign_op_pattern)]
+ for i in 1..len_points {
+ b[i] += c[i - 1];
+ // for some reason the below line makes the borrow checker mad
+ //d[i] += d[i-1]
+ d[i] = d[i] + d[i - 1];
+ c[i] /= -b[i];
+ d[i] /= -b[i];
+ }
+
+ // at this point b[i] == -a[i + 1], a[i] == 0,
+ // do row operations to eliminate 'c' coefficients and solve
+ d[len_points - 1] *= -1.;
+ #[allow(clippy::assign_op_pattern)]
+ for i in (0..len_points - 1).rev() {
+ d[i] = d[i] - (c[i] * d[i + 1]);
+ d[i] *= -1.; //d[i] /= b[i]
+ }
+
+ d
+}
diff --git a/libraries/bezier-rs/src/subpath/lookup.rs b/libraries/bezier-rs/src/subpath/lookup.rs
index c6752644f33..ed6a03b9f18 100644
--- a/libraries/bezier-rs/src/subpath/lookup.rs
+++ b/libraries/bezier-rs/src/subpath/lookup.rs
@@ -1,7 +1,6 @@
use super::*;
use crate::consts::{DEFAULT_EUCLIDEAN_ERROR_BOUND, DEFAULT_LUT_STEP_SIZE};
use crate::utils::{SubpathTValue, TValue, TValueType};
-use crate::ProjectionOptions;
use glam::DVec2;
/// Functionality relating to looking up properties of the `Subpath` or points along the `Subpath`.
@@ -25,10 +24,10 @@ impl Subpath {
}
/// Return the sum of the approximation of the length of each `Bezier` curve along the `Subpath`.
- /// - `num_subdivisions` - Number of subdivisions used to approximate the curve. The default value is `1000`.
+ /// - `tolerance` - Tolerance used to approximate the curve.
///
- pub fn length(&self, num_subdivisions: Option) -> f64 {
- self.iter().map(|bezier| bezier.length(num_subdivisions)).sum()
+ pub fn length(&self, tolerance: Option) -> f64 {
+ self.iter().map(|bezier| bezier.length(tolerance)).sum()
}
/// Converts from a subpath (composed of multiple segments) to a point along a certain segment represented.
@@ -98,9 +97,8 @@ impl Subpath {
}
/// Returns the segment index and `t` value that corresponds to the closest point on the curve to the provided point.
- /// Uses a searching algorithm akin to binary search that can be customized using the [ProjectionOptions] structure.
///
- pub fn project(&self, point: DVec2, options: Option) -> Option<(usize, f64)> {
+ pub fn project(&self, point: DVec2) -> Option<(usize, f64)> {
if self.is_empty() {
return None;
}
@@ -109,7 +107,7 @@ impl Subpath {
let (index, (_, project_t)) = self
.iter()
.map(|bezier| {
- let project_t = bezier.project(point, options);
+ let project_t = bezier.project(point);
(bezier.evaluate(TValue::Parametric(project_t)).distance(point), project_t)
})
.enumerate()
diff --git a/libraries/bezier-rs/src/subpath/mod.rs b/libraries/bezier-rs/src/subpath/mod.rs
index c720d468a6b..78e5bca8403 100644
--- a/libraries/bezier-rs/src/subpath/mod.rs
+++ b/libraries/bezier-rs/src/subpath/mod.rs
@@ -4,6 +4,7 @@ mod manipulators;
mod solvers;
mod structs;
mod transform;
+pub use core::*;
pub use structs::*;
use crate::Bezier;
diff --git a/libraries/bezier-rs/src/subpath/transform.rs b/libraries/bezier-rs/src/subpath/transform.rs
index 887d0ff3d07..0f0d95afc96 100644
--- a/libraries/bezier-rs/src/subpath/transform.rs
+++ b/libraries/bezier-rs/src/subpath/transform.rs
@@ -296,8 +296,8 @@ impl Subpath {
let start_tangent = second_bezier.non_normalized_tangent(0.);
// Compute an average unit vector, weighing the segments by a rough estimation of their relative size.
- let segment1_len = first_bezier.length(Some(5));
- let segment2_len = second_bezier.length(Some(5));
+ let segment1_len = first_bezier.length(None);
+ let segment2_len = second_bezier.length(None);
let average_unit_tangent = (end_tangent.normalize() * segment1_len + start_tangent.normalize() * segment2_len) / (segment1_len + segment2_len);
// Adjust start and end handles to fit the average tangent
diff --git a/libraries/bezier-rs/src/utils.rs b/libraries/bezier-rs/src/utils.rs
index 898e9f5eb20..13e25e00ca9 100644
--- a/libraries/bezier-rs/src/utils.rs
+++ b/libraries/bezier-rs/src/utils.rs
@@ -90,11 +90,6 @@ pub fn compute_abc_for_cubic_through_points(start_point: DVec2, point_on_curve:
compute_abc_through_points(start_point, point_on_curve, end_point, t_cubed, cubed_one_minus_t)
}
-/// Return the index and the value of the closest point in the LUT compared to the provided point.
-pub fn get_closest_point_in_lut(lut: &[DVec2], point: DVec2) -> (usize, f64) {
- lut.iter().enumerate().map(|(i, p)| (i, point.distance_squared(*p))).min_by(|x, y| (x.1).total_cmp(&(y.1))).unwrap()
-}
-
/// Find the roots of the linear equation `ax + b`.
pub fn solve_linear(a: f64, b: f64) -> [Option; 3] {
// There exist roots when `a` is not 0
diff --git a/libraries/dyn-any/src/lib.rs b/libraries/dyn-any/src/lib.rs
index 7988639ec3e..ef75825f16a 100644
--- a/libraries/dyn-any/src/lib.rs
+++ b/libraries/dyn-any/src/lib.rs
@@ -259,10 +259,13 @@ impl_type!(
);
#[cfg(feature = "std")]
-use std::sync::*;
+use std::{
+ collections::{HashMap, HashSet},
+ sync::*,
+};
#[cfg(feature = "std")]
-impl_type!(Once, Mutex, RwLock);
+impl_type!(Once, Mutex, RwLock, HashSet, HashMap);
#[cfg(feature = "rc")]
use std::rc::Rc;
diff --git a/node-graph/gcore/src/graphic_element/renderer.rs b/node-graph/gcore/src/graphic_element/renderer.rs
index cf6c165f875..8c788aee638 100644
--- a/node-graph/gcore/src/graphic_element/renderer.rs
+++ b/node-graph/gcore/src/graphic_element/renderer.rs
@@ -296,7 +296,10 @@ impl GraphicElementRendered for VectorData {
let transformed_bounds = self.bounding_box_with_transform(multiplied_transform).unwrap_or_default();
let mut path = String::new();
- for subpath in &self.subpaths {
+ for (_, subpath) in self.region_bézier_paths() {
+ let _ = subpath.subpath_to_svg(&mut path, multiplied_transform);
+ }
+ for subpath in self.stroke_bézier_paths() {
let _ = subpath.subpath_to_svg(&mut path, multiplied_transform);
}
diff --git a/node-graph/gcore/src/uuid.rs b/node-graph/gcore/src/uuid.rs
index b18a20be6b8..44827e641b1 100644
--- a/node-graph/gcore/src/uuid.rs
+++ b/node-graph/gcore/src/uuid.rs
@@ -89,4 +89,8 @@ impl ManipulatorGroupId {
self.0 += 1;
Self(old)
}
+
+ pub(crate) fn inner(self) -> u64 {
+ self.0
+ }
}
diff --git a/node-graph/gcore/src/vector/generator_nodes.rs b/node-graph/gcore/src/vector/generator_nodes.rs
index 1f63ca2ebbb..168d7f62ad1 100644
--- a/node-graph/gcore/src/vector/generator_nodes.rs
+++ b/node-graph/gcore/src/vector/generator_nodes.rs
@@ -6,6 +6,8 @@ use bezier_rs::Subpath;
use glam::DVec2;
+use super::PointId;
+
#[derive(Debug, Clone, Copy)]
pub struct CircleGenerator {
radius: Radius,
@@ -43,7 +45,7 @@ fn square_generator(_input: (), size_x: f64, size_y: f64) -> VectorData {
let corner1 = -size / 2.;
let corner2 = size / 2.;
- super::VectorData::from_subpaths(vec![Subpath::new_rect(corner1, corner2)])
+ super::VectorData::from_subpath(Subpath::new_rect(corner1, corner2))
}
#[derive(Debug, Clone, Copy)]
@@ -83,7 +85,7 @@ pub struct LineGenerator {
#[node_macro::node_fn(LineGenerator)]
fn line_generator(_input: (), pos_1: DVec2, pos_2: DVec2) -> VectorData {
- super::VectorData::from_subpaths(vec![Subpath::new_line(pos_1, pos_2)])
+ super::VectorData::from_subpath(Subpath::new_line(pos_1, pos_2))
}
#[derive(Debug, Clone, Copy)]
@@ -93,7 +95,7 @@ pub struct SplineGenerator {
#[node_macro::node_fn(SplineGenerator)]
fn spline_generator(_input: (), positions: Vec) -> VectorData {
- super::VectorData::from_subpaths(vec![Subpath::new_cubic_spline(positions)])
+ super::VectorData::from_subpath(Subpath::new_cubic_spline(positions))
}
// TODO(TrueDoctor): I removed the Arc requirement we should think about when it makes sense to use it vs making a generic value node
diff --git a/node-graph/gcore/src/vector/vector_data.rs b/node-graph/gcore/src/vector/vector_data.rs
index 631cadbc43f..3417765ebaf 100644
--- a/node-graph/gcore/src/vector/vector_data.rs
+++ b/node-graph/gcore/src/vector/vector_data.rs
@@ -7,6 +7,9 @@ use dyn_any::{DynAny, StaticType};
use glam::{DAffine2, DVec2};
+mod attributes;
+pub use attributes::*;
+
/// [VectorData] is passed between nodes.
/// It contains a list of subpaths (that may be open or closed), a transform, and some style information.
#[derive(Clone, Debug, PartialEq, DynAny)]
@@ -19,6 +22,10 @@ pub struct VectorData {
/// A list of all manipulator groups (referenced in `subpaths`) that have smooth handles (where their handles are colinear, or locked to 180° angles from one another)
/// This gets read in `graph_operation_message_handler.rs` by calling `inputs.as_mut_slice()` (search for the string `"Shape does not have subpath and mirror angle inputs"` to find it).
pub mirror_angle: Vec,
+
+ pub point_domain: PointDomain,
+ pub segment_domain: SegmentDomain,
+ pub region_domain: RegionDomain,
}
impl core::hash::Hash for VectorData {
@@ -40,26 +47,53 @@ impl VectorData {
style: PathStyle::new(Some(Stroke::new(Some(Color::BLACK), 0.)), super::style::Fill::None),
alpha_blending: AlphaBlending::new(),
mirror_angle: Vec::new(),
+ point_domain: PointDomain::new(),
+ segment_domain: SegmentDomain::new(),
+ region_domain: RegionDomain::new(),
}
}
-
- /// Iterator over the manipulator groups of the subpaths
- pub fn manipulator_groups(&self) -> impl Iterator
- > + DoubleEndedIterator {
- self.subpaths.iter().flat_map(|subpath| subpath.manipulator_groups())
- }
-
- pub fn manipulator_from_id(&self, id: ManipulatorGroupId) -> Option<&ManipulatorGroup> {
- self.subpaths.iter().find_map(|subpath| subpath.manipulator_from_id(id))
- }
-
/// Construct some new vector data from a single subpath with an identity transform and black fill.
pub fn from_subpath(subpath: bezier_rs::Subpath) -> Self {
- Self::from_subpaths(vec![subpath])
+ Self::from_subpaths([subpath])
}
/// Construct some new vector data from subpaths with an identity transform and black fill.
- pub fn from_subpaths(subpaths: Vec>) -> Self {
- super::VectorData { subpaths, ..Self::empty() }
+ pub fn from_subpaths(subpaths: impl IntoIterator
- >) -> Self {
+ let mut vector_data = Self::empty();
+
+ for subpath in subpaths.into_iter() {
+ for point in subpath.manipulator_groups() {
+ vector_data.point_domain.push(point.id.into(), point.anchor);
+ }
+
+ let handles = |a: &ManipulatorGroup<_>, b: &ManipulatorGroup<_>| match (a.out_handle, b.in_handle) {
+ (None, None) => bezier_rs::BezierHandles::Linear,
+ (Some(handle), None) | (None, Some(handle)) => bezier_rs::BezierHandles::Quadratic { handle },
+ (Some(handle_start), Some(handle_end)) => bezier_rs::BezierHandles::Cubic { handle_start, handle_end },
+ };
+ let [mut first_seg, mut last_seg] = [None, None];
+ for x in subpath.manipulator_groups().windows(2) {
+ let id = SegmentId::generate();
+ first_seg = Some(first_seg.unwrap_or(id));
+ last_seg = Some(id);
+ vector_data.segment_domain.push(id, x[0].id.into(), x[1].id.into(), handles(&x[0], &x[1]), StrokeId::generate());
+ }
+
+ if subpath.closed() {
+ if let (Some(last), Some(first)) = (subpath.manipulator_groups().last(), subpath.manipulator_groups().first()) {
+ let id = SegmentId::generate();
+ first_seg = Some(first_seg.unwrap_or(id));
+ last_seg = Some(id);
+ vector_data.segment_domain.push(id, last.id.into(), first.id.into(), handles(last, first), StrokeId::generate());
+ }
+
+ if let [Some(first_seg), Some(last_seg)] = [first_seg, last_seg] {
+ vector_data.region_domain.push(RegionId::generate(), first_seg..=last_seg, FillId::generate());
+ }
+ }
+ }
+
+ vector_data
}
/// Compute the bounding boxes of the subpaths without any transform
@@ -69,9 +103,8 @@ impl VectorData {
/// Compute the bounding boxes of the subpaths with the specified transform
pub fn bounding_box_with_transform(&self, transform: DAffine2) -> Option<[DVec2; 2]> {
- self.subpaths
- .iter()
- .filter_map(|subpath| subpath.bounding_box_with_transform(transform))
+ self.segment_bézier_iter()
+ .map(|(_, bézier, _, _)| bézier.apply_transformation(|point| transform.transform_point2(point)).bounding_box())
.reduce(|b1, b2| [b1[0].min(b2[0]), b1[1].max(b2[1])])
}
diff --git a/node-graph/gcore/src/vector/vector_data/attributes.rs b/node-graph/gcore/src/vector/vector_data/attributes.rs
new file mode 100644
index 00000000000..cf69e1d3dd7
--- /dev/null
+++ b/node-graph/gcore/src/vector/vector_data/attributes.rs
@@ -0,0 +1,330 @@
+use std::collections::HashMap;
+
+use dyn_any::{DynAny, StaticType};
+use glam::{DAffine2, DVec2};
+
+macro_rules! create_ids {
+ ($($id:ident),*) => {
+ $(
+ #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, DynAny)]
+ #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+ /// A strongly typed id
+ pub struct $id(u64);
+
+ impl $id {
+ /// Generate a new random id
+ pub fn generate() -> Self{
+ Self(crate::uuid::generate_uuid())
+ }
+ }
+ )*
+ };
+}
+
+create_ids! { PointId, SegmentId, RegionId, StrokeId, FillId }
+
+#[derive(Clone, Debug, Default, PartialEq, DynAny)]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+/// Stores data which is per point. Each point is merely a position and can be used in a point cloud or to for a bézier path. In future this will be extendable at runtime with custom attributes.
+pub struct PointDomain {
+ id: Vec,
+ positions: Vec,
+}
+
+impl PointDomain {
+ pub const fn new() -> Self {
+ Self {
+ id: Vec::new(),
+ positions: Vec::new(),
+ }
+ }
+ fn resolve_id(&self, id: PointId) -> Option {
+ self.id.iter().position(|&check_id| check_id == id)
+ }
+ pub fn clear(&mut self) {
+ self.id.clear();
+ self.positions.clear();
+ }
+ pub fn push(&mut self, id: PointId, position: DVec2) {
+ self.id.push(id);
+ self.positions.push(position);
+ }
+ pub fn positions(&self) -> &[DVec2] {
+ &self.positions
+ }
+ pub fn ids(&self) -> &[PointId] {
+ &self.id
+ }
+
+ pub fn pos_from_id(&self, id: PointId) -> Option {
+ let pos = self.resolve_id(id).map(|index| self.positions[index]);
+ if pos.is_none() {
+ warn!("Resolving pos of invalid id");
+ }
+ pos
+ }
+ fn concat(&mut self, other: &Self, transform: DAffine2, id_map: &IdMap) {
+ self.id.extend(other.id.iter().map(|id| *id_map.point_map.get(id).unwrap_or(id)));
+ self.positions.extend(other.positions.iter().map(|&pos| transform.transform_point2(pos)));
+ }
+}
+
+#[derive(Clone, Debug, Default, PartialEq, DynAny)]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+/// Stores data which is per segment. A segment is a bézier curve between two points with a stroke. In future this will be extendable at runtime with custom attributes.
+pub struct SegmentDomain {
+ ids: Vec,
+ start_point: Vec,
+ end_point: Vec,
+ handles: Vec,
+ stroke: Vec,
+}
+impl SegmentDomain {
+ pub const fn new() -> Self {
+ Self {
+ ids: Vec::new(),
+ start_point: Vec::new(),
+ end_point: Vec::new(),
+ handles: Vec::new(),
+ stroke: Vec::new(),
+ }
+ }
+ fn resolve_id(&self, id: SegmentId) -> Option {
+ self.ids.iter().position(|&check_id| check_id == id)
+ }
+ pub fn clear(&mut self) {
+ self.ids.clear();
+ self.start_point.clear();
+ self.end_point.clear();
+ self.handles.clear();
+ self.stroke.clear();
+ }
+ pub fn push(&mut self, id: SegmentId, start: PointId, end: PointId, handles: bezier_rs::BezierHandles, stroke: StrokeId) {
+ self.ids.push(id);
+ self.start_point.push(start);
+ self.end_point.push(end);
+ self.handles.push(handles);
+ self.stroke.push(stroke);
+ }
+ fn resolve_range(&self, range: &core::ops::RangeInclusive) -> Option> {
+ match (self.resolve_id(*range.start()), self.resolve_id(*range.end())) {
+ (Some(start), Some(end)) => Some(start..=end),
+ _ => {
+ warn!("Resolving range with invalid id");
+ None
+ }
+ }
+ }
+ fn concat(&mut self, other: &Self, transform: DAffine2, id_map: &IdMap) {
+ self.ids.extend(other.ids.iter().map(|id| *id_map.segment_map.get(id).unwrap_or(id)));
+ self.start_point.extend(other.start_point.iter().map(|id| *id_map.point_map.get(id).unwrap_or(id)));
+ self.end_point.extend(other.end_point.iter().map(|id| *id_map.point_map.get(id).unwrap_or(id)));
+ self.handles.extend(other.handles.iter().map(|handles| handles.apply_transformation(|p| transform.transform_point2(p))));
+ self.stroke.extend(&other.stroke);
+ }
+}
+
+#[derive(Clone, Debug, Default, PartialEq, DynAny)]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+/// Stores data which is per segment. A region is an encolsed area composed of a range of segments from the [`SegmentDomain`] that can be given a fill. In future this will be extendable at runtime with custom attributes.
+pub struct RegionDomain {
+ ids: Vec,
+ segment_range: Vec>,
+ fill: Vec,
+}
+
+impl RegionDomain {
+ pub const fn new() -> Self {
+ Self {
+ ids: Vec::new(),
+ segment_range: Vec::new(),
+ fill: Vec::new(),
+ }
+ }
+ fn resolve_id(&self, id: RegionId) -> Option {
+ self.ids.iter().position(|&check_id| check_id == id)
+ }
+ pub fn clear(&mut self) {
+ self.ids.clear();
+ self.segment_range.clear();
+ self.fill.clear();
+ }
+ pub fn push(&mut self, id: RegionId, segment_range: core::ops::RangeInclusive, fill: FillId) {
+ self.ids.push(id);
+ self.segment_range.push(segment_range);
+ self.fill.push(fill);
+ }
+ fn concat(&mut self, other: &Self, _transform: DAffine2, id_map: &IdMap) {
+ self.ids.extend(other.ids.iter().map(|id| *id_map.region_map.get(id).unwrap_or(id)));
+ self.segment_range.extend(
+ other
+ .segment_range
+ .iter()
+ .map(|range| *id_map.segment_map.get(range.start()).unwrap_or(range.start())..=*id_map.segment_map.get(range.end()).unwrap_or(range.end())),
+ );
+ self.fill.extend(&other.fill);
+ }
+}
+
+impl super::VectorData {
+ /// Construct a [`bezier_rs::Bezier`] curve spanning from the resolved position of the start and end points with the specified handles. Returns None if either id is invalid.
+ fn segment_to_bézier(&self, start: PointId, end: PointId, handles: bezier_rs::BezierHandles) -> Option {
+ let start = self.point_domain.pos_from_id(start)?;
+ let end = self.point_domain.pos_from_id(end)?;
+ Some(bezier_rs::Bezier { start, end, handles })
+ }
+
+ /// Tries to convert a segment with the specified id to a [`bezier_rs::Bezier`], returning None if the id is invalid.
+ pub fn segment_from_id(&self, id: SegmentId) -> Option {
+ let index = self.segment_domain.resolve_id(id)?;
+ self.segment_to_bézier(self.segment_domain.start_point[index], self.segment_domain.end_point[index], self.segment_domain.handles[index])
+ }
+
+ /// Iterator over all of the [`bezier_rs::Bezier`] following the order that they are stored in the segment domain, skipping invalid segments.
+ pub fn segment_bézier_iter(&self) -> impl Iterator
- + '_ {
+ let to_bezier = |(((&handles, &id), &start), &end)| self.segment_to_bézier(start, end, handles).map(|bézier| (id, bézier, start, end));
+ self.segment_domain
+ .handles
+ .iter()
+ .zip(&self.segment_domain.ids)
+ .zip(&self.segment_domain.start_point)
+ .zip(&self.segment_domain.end_point)
+ .filter_map(to_bezier)
+ }
+
+ /// 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 semgents are not continous.
+ fn subpath_from_segments(&self, segments: impl Iterator
- ) -> Option> {
+ let mut first_point = None;
+ let mut groups = Vec::new();
+ let mut last: Option<(PointId, bezier_rs::BezierHandles)> = None;
+ let end_point = |last: Option<(PointId, bezier_rs::BezierHandles)>, next: Option, groups: &mut Vec<_>| {
+ if let Some((disconnected_previous, previous_handle)) = last.filter(|(end, _)| !next.is_some_and(|next| next == *end)) {
+ groups.push(bezier_rs::ManipulatorGroup {
+ anchor: self.point_domain.pos_from_id(disconnected_previous)?,
+ in_handle: previous_handle.end(),
+ out_handle: None,
+ id: disconnected_previous,
+ });
+ }
+ Some(())
+ };
+
+ for (handle, start, end) in segments {
+ if last.is_some_and(|(previous_end, _)| previous_end != start) {
+ warn!("subpath_from_segments that were not continous");
+ return None;
+ }
+ first_point = Some(first_point.unwrap_or(start));
+ end_point(last, Some(start), &mut groups)?;
+
+ groups.push(bezier_rs::ManipulatorGroup {
+ anchor: self.point_domain.pos_from_id(start)?,
+ in_handle: last.and_then(|(_, handle)| handle.end()),
+ out_handle: handle.start(),
+ id: start,
+ });
+
+ last = Some((end, handle));
+ }
+ end_point(last, None, &mut groups)?;
+ let closed = groups.len() > 1 && last.map(|(point, _)| point) == first_point;
+ Some(bezier_rs::Subpath::new(groups, closed))
+ }
+
+ /// Construct a [`bezier_rs::Bezier`] curve for each region, skipping invalid regions.
+ pub fn region_bézier_paths(&self) -> impl Iterator
- )> + '_ {
+ self.region_domain
+ .ids
+ .iter()
+ .zip(&self.region_domain.segment_range)
+ .filter_map(|(&id, segment_range)| self.segment_domain.resolve_range(segment_range).map(|range| (id, range)))
+ .filter_map(|(id, range)| {
+ let segments_iter = self.segment_domain.handles[range.clone()]
+ .iter()
+ .zip(&self.segment_domain.start_point[range.clone()])
+ .zip(&self.segment_domain.end_point[range])
+ .map(|((&handles, &start), &end)| (handles, start, end));
+
+ self.subpath_from_segments(segments_iter).map(|subpath| (id, subpath))
+ })
+ }
+
+ /// Construct a [`bezier_rs::Bezier`] curve for stroke.
+ pub fn stroke_bézier_paths(&self) -> StrokePathIter<'_> {
+ StrokePathIter { vector_data: self, segment_index: 0 }
+ }
+}
+
+pub struct StrokePathIter<'a> {
+ vector_data: &'a super::VectorData,
+ segment_index: usize,
+}
+
+impl<'a> Iterator for StrokePathIter<'a> {
+ type Item = bezier_rs::Subpath;
+
+ fn next(&mut self) -> Option {
+ let segments = &self.vector_data.segment_domain;
+ if self.segment_index >= segments.end_point.len() {
+ return None;
+ }
+ let mut old_end = None;
+ let mut count = 0;
+ let segments_iter = segments.handles[self.segment_index..]
+ .iter()
+ .zip(&segments.start_point[self.segment_index..])
+ .zip(&segments.end_point[self.segment_index..])
+ .map(|((&handles, &start), &end)| (handles, start, end))
+ .take_while(|&(_, start, end)| {
+ let continous = old_end.is_none() || old_end.is_some_and(|old_end| old_end == start);
+ old_end = Some(end);
+ count += 1;
+ continous
+ });
+
+ let subpath = self.vector_data.subpath_from_segments(segments_iter);
+ self.segment_index += count;
+ subpath
+ }
+}
+
+impl bezier_rs::Identifier for PointId {
+ fn new() -> Self {
+ Self::new()
+ }
+}
+impl From for PointId {
+ fn from(value: crate::uuid::ManipulatorGroupId) -> Self {
+ Self(value.inner())
+ }
+}
+
+impl crate::vector::ConcatElement for super::VectorData {
+ fn concat(&mut self, other: &Self, transform: glam::DAffine2) {
+ let new_ids = other.point_domain.id.iter().filter(|id| self.point_domain.id.contains(id)).map(|&old| (old, PointId::generate()));
+ let point_map = new_ids.collect::>();
+ let new_ids = other
+ .segment_domain
+ .ids
+ .iter()
+ .filter(|id| self.segment_domain.ids.contains(id))
+ .map(|&old| (old, SegmentId::generate()));
+ let segment_map = new_ids.collect::>();
+ let new_ids = other.region_domain.ids.iter().filter(|id| self.region_domain.ids.contains(id)).map(|&old| (old, RegionId::generate()));
+ let region_map = new_ids.collect::>();
+ let id_map = IdMap { point_map, segment_map, region_map };
+ self.point_domain.concat(&other.point_domain, transform * other.transform, &id_map);
+ self.segment_domain.concat(&other.segment_domain, transform * other.transform, &id_map);
+ self.region_domain.concat(&other.region_domain, transform * other.transform, &id_map);
+ // TODO: properly deal with fills such as gradients
+ self.style = other.style.clone();
+ self.mirror_angle.extend(other.mirror_angle.iter().copied());
+ self.alpha_blending = other.alpha_blending;
+ }
+}
+
+struct IdMap {
+ point_map: HashMap,
+ segment_map: HashMap,
+ region_map: HashMap,
+}
diff --git a/node-graph/gcore/src/vector/vector_nodes.rs b/node-graph/gcore/src/vector/vector_nodes.rs
index 49ab0df9240..a21f8d1b9d2 100644
--- a/node-graph/gcore/src/vector/vector_nodes.rs
+++ b/node-graph/gcore/src/vector/vector_nodes.rs
@@ -1,11 +1,11 @@
use super::style::{Fill, FillType, Gradient, GradientType, Stroke};
-use super::VectorData;
+use super::{PointId, SegmentId, StrokeId, VectorData};
use crate::renderer::GraphicElementRendered;
use crate::transform::{Footprint, Transform, TransformMut};
use crate::{Color, GraphicGroup, Node};
use core::future::Future;
-use bezier_rs::{Subpath, SubpathTValue, TValue};
+use bezier_rs::{Subpath, TValue};
use glam::{DAffine2, DVec2};
use rand::{Rng, SeedableRng};
@@ -85,23 +85,17 @@ pub struct RepeatNode {
}
#[node_macro::node_fn(RepeatNode)]
-fn repeat_vector_data(mut vector_data: VectorData, direction: DVec2, count: u32) -> VectorData {
+fn repeat_vector_data(vector_data: VectorData, direction: DVec2, count: u32) -> VectorData {
// repeat the vector data
- let VectorData { subpaths, transform, .. } = &vector_data;
-
- let mut new_subpaths: Vec> = Vec::with_capacity(subpaths.len() * count as usize);
- let inverse = transform.inverse();
+ let mut result = VectorData::empty();
+ let inverse = vector_data.transform.inverse();
let direction = inverse.transform_vector2(direction);
for i in 0..count {
let transform = DAffine2::from_translation(direction * i as f64);
- for mut subpath in subpaths.clone() {
- subpath.apply_transform(transform);
- new_subpaths.push(subpath);
- }
+ result.concat(&vector_data, transform);
}
- vector_data.subpaths = new_subpaths;
- vector_data
+ result
}
#[derive(Debug, Clone, Copy)]
@@ -112,8 +106,8 @@ pub struct CircularRepeatNode {
}
#[node_macro::node_fn(CircularRepeatNode)]
-fn circular_repeat_vector_data(mut vector_data: VectorData, angle_offset: f64, radius: f64, count: u32) -> VectorData {
- let mut new_subpaths: Vec> = Vec::with_capacity(vector_data.subpaths.len() * count as usize);
+fn circular_repeat_vector_data(vector_data: VectorData, angle_offset: f64, radius: f64, count: u32) -> VectorData {
+ let mut result = VectorData::empty();
let Some(bounding_box) = vector_data.bounding_box() else { return vector_data };
let center = (bounding_box[0] + bounding_box[1]) / 2.;
@@ -124,14 +118,10 @@ fn circular_repeat_vector_data(mut vector_data: VectorData, angle_offset: f64, r
let angle = (2. * std::f64::consts::PI / count as f64) * i as f64 + angle_offset.to_radians();
let rotation = DAffine2::from_angle(angle);
let transform = DAffine2::from_translation(center) * rotation * DAffine2::from_translation(base_transform);
- for mut subpath in vector_data.subpaths.clone() {
- subpath.apply_transform(transform);
- new_subpaths.push(subpath);
- }
+ result.concat(&vector_data, transform);
}
- vector_data.subpaths = new_subpaths;
- vector_data
+ result
}
#[derive(Debug, Clone, Copy)]
@@ -140,29 +130,16 @@ pub struct BoundingBoxNode;
#[node_macro::node_fn(BoundingBoxNode)]
fn generate_bounding_box(vector_data: VectorData) -> VectorData {
let bounding_box = vector_data.bounding_box().unwrap();
- VectorData::from_subpaths(vec![Subpath::new_rect(
+ VectorData::from_subpath(Subpath::new_rect(
vector_data.transform.transform_point2(bounding_box[0]),
vector_data.transform.transform_point2(bounding_box[1]),
- )])
+ ))
}
pub trait ConcatElement {
fn concat(&mut self, other: &Self, transform: DAffine2);
}
-impl ConcatElement for VectorData {
- fn concat(&mut self, other: &Self, transform: DAffine2) {
- for mut subpath in other.subpaths.iter().cloned() {
- subpath.apply_transform(transform * other.transform);
- self.subpaths.push(subpath);
- }
- // TODO: properly deal with fills such as gradients
- self.style = other.style.clone();
- self.mirror_angle.extend(other.mirror_angle.iter().copied());
- self.alpha_blending = other.alpha_blending;
- }
-}
-
impl ConcatElement for GraphicGroup {
fn concat(&mut self, other: &Self, transform: DAffine2) {
// TODO: Decide if we want to keep this behavior whereby the layers are flattened
@@ -198,7 +175,7 @@ async fn copy_to_points 1e-6;
let mut result = I::default();
- for point in points_list {
+ for &point in points_list {
let center_transform = DAffine2::from_translation(instance_center);
let translation = points.transform.transform_point2(point);
@@ -253,7 +230,7 @@ pub struct SamplePoints, FL: Future