diff --git a/Cargo.lock b/Cargo.lock index c14d70e63b..86cd385014 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2366,6 +2366,7 @@ dependencies = [ "serde", "specta", "spirv-std", + "tokio", "usvg 0.39.0", "wasm-bindgen", "web-sys", diff --git a/editor/src/messages/portfolio/document/overlays/utility_types.rs b/editor/src/messages/portfolio/document/overlays/utility_types.rs index 98c7ed0795..d8c2a98e29 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types.rs @@ -4,7 +4,6 @@ use crate::messages::prelude::Message; use bezier_rs::Subpath; use graphene_core::renderer::Quad; -use graphene_core::uuid::ManipulatorGroupId; use core::f64::consts::PI; use glam::{DAffine2, DVec2}; @@ -114,7 +113,7 @@ impl OverlayContext { self.render_context.stroke(); } - pub fn outline<'a>(&mut self, subpaths: impl Iterator>, transform: DAffine2) { + pub fn outline<'a, Id: bezier_rs::Identifier>(&mut self, subpaths: impl Iterator>, transform: DAffine2) { self.render_context.begin_path(); for subpath in subpaths { let mut curves = subpath.iter().peekable(); diff --git a/editor/src/messages/portfolio/document/utility_types/document_metadata.rs b/editor/src/messages/portfolio/document/utility_types/document_metadata.rs index fdfabb7b42..ca4d6efda5 100644 --- a/editor/src/messages/portfolio/document/utility_types/document_metadata.rs +++ b/editor/src/messages/portfolio/document/utility_types/document_metadata.rs @@ -4,9 +4,9 @@ use graph_craft::document::{DocumentNode, NodeId, NodeNetwork}; use graphene_core::renderer::ClickTarget; use graphene_core::renderer::Quad; use graphene_core::transform::Footprint; -use graphene_core::uuid::ManipulatorGroupId; use glam::{DAffine2, DVec2}; +use graphene_std::vector::PointId; use std::collections::{HashMap, HashSet}; use std::num::NonZeroU64; @@ -287,7 +287,7 @@ impl DocumentMetadata { .reduce(Quad::combine_bounds) } - pub fn layer_outline(&self, layer: LayerNodeIdentifier) -> impl Iterator> { + pub fn layer_outline(&self, layer: LayerNodeIdentifier) -> impl Iterator> { static EMPTY: Vec = Vec::new(); let click_targets = self.click_targets.get(&layer).unwrap_or(&EMPTY); click_targets.iter().map(|click_target| &click_target.subpath) diff --git a/editor/src/messages/tool/common_functionality/shape_editor.rs b/editor/src/messages/tool/common_functionality/shape_editor.rs index d6d1d06f99..059e0168df 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.rs b/editor/src/messages/tool/common_functionality/snapping.rs index ec5ad9df06..7eb95cdf41 100644 --- a/editor/src/messages/tool/common_functionality/snapping.rs +++ b/editor/src/messages/tool/common_functionality/snapping.rs @@ -172,8 +172,8 @@ impl<'a> SnapData<'a> { fn ignore_bounds(&self, layer: LayerNodeIdentifier) -> bool { self.manipulators.iter().any(|&(ignore, _)| ignore == layer) } - fn ignore_manipulator(&self, layer: LayerNodeIdentifier, manipulator: ManipulatorGroupId) -> bool { - self.manipulators.contains(&(layer, manipulator)) + fn ignore_manipulator(&self, layer: LayerNodeIdentifier, manipulator: impl Into) -> bool { + self.manipulators.contains(&(layer, manipulator.into())) } } impl SnapManager { @@ -327,7 +327,7 @@ impl SnapManager { if let Some(ind) = &self.indicator { for curve in &ind.curves { let Some(curve) = curve else { continue }; - overlay_context.outline([Subpath::from_bezier(curve)].iter(), to_viewport); + overlay_context.outline::([Subpath::from_bezier(curve)].iter(), to_viewport); } if let Some(quad) = ind.target_bounds { overlay_context.quad(to_viewport * quad); 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 7a5418b936..1c4984d22d 100644 --- a/editor/src/messages/tool/common_functionality/snapping/layer_snapper.rs +++ b/editor/src/messages/tool/common_functionality/snapping/layer_snapper.rs @@ -9,6 +9,7 @@ use bezier_rs::{Bezier, Identifier, Subpath, TValue}; use glam::{DAffine2, DVec2}; use graphene_core::renderer::Quad; use graphene_core::uuid::ManipulatorGroupId; +use graphene_std::vector::PointId; #[derive(Clone, Debug, Default)] pub struct LayerSnapper { @@ -62,7 +63,7 @@ impl LayerSnapper { for subpath in document.metadata.layer_outline(layer) { for (start_index, curve) in subpath.iter().enumerate() { let document_curve = curve.apply_transformation(|p| transform.transform_point2(p)); - let start = subpath.manipulator_groups()[start_index].id; + let start = subpath.manipulator_groups()[start_index].id.into(); if snap_data.ignore_manipulator(layer, start) || snap_data.ignore_manipulator(layer, subpath.manipulator_groups()[(start_index + 1) % subpath.len()].id) { continue; } @@ -94,7 +95,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); @@ -372,7 +373,7 @@ pub fn get_bbox_points(quad: Quad, points: &mut Vec, values: fn handle_not_under(to_document: DAffine2) -> impl Fn(&DVec2) -> bool { move |&offset: &DVec2| to_document.transform_vector2(offset).length_squared() >= HIDE_HANDLE_DISTANCE * HIDE_HANDLE_DISTANCE } -fn subpath_anchor_snap_points(layer: LayerNodeIdentifier, subpath: &Subpath, snap_data: &SnapData, points: &mut Vec, to_document: DAffine2) { +fn subpath_anchor_snap_points(layer: LayerNodeIdentifier, subpath: &Subpath, snap_data: &SnapData, points: &mut Vec, to_document: DAffine2) { let document = snap_data.document; // Midpoints of linear segments if document.snapping_state.target_enabled(SnapTarget::Geometry(GeometrySnapTarget::LineMidpoint)) { @@ -418,7 +419,7 @@ fn subpath_anchor_snap_points(layer: LayerNodeIdentifier, subpath: &Subpath, to_document: DAffine2, subpath: &Subpath, index: usize) -> bool { +pub fn group_smooth(group: &bezier_rs::ManipulatorGroup, to_document: DAffine2, subpath: &Subpath, index: usize) -> bool { let anchor = group.anchor; let handle_in = group.in_handle.map(|handle| anchor - handle).filter(handle_not_under(to_document)); let handle_out = group.out_handle.map(|handle| handle - anchor).filter(handle_not_under(to_document)); diff --git a/libraries/bezier-rs/src/bezier/lookup.rs b/libraries/bezier-rs/src/bezier/lookup.rs index f4dc8cbcc0..9b3ef2efd5 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 5224783b7a..e56e8d9218 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 5831ee5574..0285d777f2 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, @@ -31,10 +31,55 @@ pub enum BezierHandles { handle_end: DVec2, }, } + +impl std::hash::Hash for BezierHandles { + fn hash(&self, state: &mut H) { + std::mem::discriminant(self).hash(state); + match self { + BezierHandles::Linear => {} + BezierHandles::Quadratic { handle } => handle.to_array().map(|v| v.to_bits()).hash(state), + BezierHandles::Cubic { handle_start, handle_end } => [handle_start, handle_end].map(|handle| handle.to_array().map(|v| v.to_bits())).hash(state), + } + } +} + 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 handle point in the Bezier. + #[must_use] + 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 04b9e38f6f..088d541bef 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 69f45b0ef5..94460e11d9 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/consts.rs b/libraries/bezier-rs/src/consts.rs index d7b1843890..bef83def97 100644 --- a/libraries/bezier-rs/src/consts.rs +++ b/libraries/bezier-rs/src/consts.rs @@ -4,8 +4,6 @@ pub const MAX_ABSOLUTE_DIFFERENCE: f64 = 1e-3; /// A stricter constant used to determine if `f64`s are equivalent. pub const STRICT_MAX_ABSOLUTE_DIFFERENCE: f64 = 1e-6; -/// Number of distances used in search algorithm for `project`. -pub const NUM_DISTANCES: usize = 5; /// Maximum allowed angle that the normal of the `start` or `end` point can make with the normal of the corresponding handle for a curve to be considered scalable/simple. pub const SCALABLE_CURVE_MAX_ENDPOINT_NORMAL_ANGLE: f64 = std::f64::consts::PI / 3.; /// Minimum allowable separation between adjacent `t` values when calculating curve intersections @@ -19,8 +17,6 @@ pub const DEFAULT_EUCLIDEAN_ERROR_BOUND: f64 = 0.001; pub const DEFAULT_T_VALUE: f64 = 0.5; /// Default LUT step size in `compute_lookup_table` function. pub const DEFAULT_LUT_STEP_SIZE: usize = 10; -/// Default number of subdivisions used in `length` calculation. -pub const DEFAULT_LENGTH_SUBDIVISIONS: usize = 1000; /// Default step size for `reduce` function. pub const DEFAULT_REDUCE_STEP_SIZE: f64 = 0.01; diff --git a/libraries/bezier-rs/src/subpath/core.rs b/libraries/bezier-rs/src/subpath/core.rs index 05be64ca74..31f0ade32c 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 } @@ -276,61 +277,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 c6752644f3..ed6a03b9f1 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 c720d468a6..78e5bca840 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 887d0ff3d0..0f0d95afc9 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 898e9f5eb2..13e25e00ca 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 7988639ec3..ef75825f16 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/Cargo.toml b/node-graph/gcore/Cargo.toml index 3400f4ad4b..96fca5c0a5 100644 --- a/node-graph/gcore/Cargo.toml +++ b/node-graph/gcore/Cargo.toml @@ -57,10 +57,11 @@ num-derive = { workspace = true } num-traits = { workspace = true, default-features = false, features = ["i128"] } wasm-bindgen = { workspace = true, optional = true } js-sys = { workspace = true, optional = true } +web-sys = { workspace = true, optional = true, features = [ + "HtmlCanvasElement", +] } usvg = { workspace = true } rand = { workspace = true, default-features = false, features = ["std_rng"] } -[dependencies.web-sys] -workspace = true -optional = true -features = ["HtmlCanvasElement"] +[dev-dependencies] +tokio = { workspace = true, features = ["rt", "macros"] } diff --git a/node-graph/gcore/src/graphic_element.rs b/node-graph/gcore/src/graphic_element.rs index 6167c6e2fb..a8ade3e7aa 100644 --- a/node-graph/gcore/src/graphic_element.rs +++ b/node-graph/gcore/src/graphic_element.rs @@ -263,7 +263,7 @@ impl GraphicElement { let mut builder = PathBuilder::new(); let transform = to_transform(vector_data.transform); - for subpath in vector_data.subpaths.iter() { + for subpath in vector_data.stroke_bezier_paths() { let start = vector_data.transform.transform_point2(subpath[0].anchor); builder.move_to(start.x as f32, start.y as f32); for bezier in subpath.iter() { diff --git a/node-graph/gcore/src/graphic_element/renderer.rs b/node-graph/gcore/src/graphic_element/renderer.rs index 5ae1987cae..cfb6222725 100644 --- a/node-graph/gcore/src/graphic_element/renderer.rs +++ b/node-graph/gcore/src/graphic_element/renderer.rs @@ -2,7 +2,8 @@ mod quad; use crate::raster::{BlendMode, Image, ImageFrame}; use crate::transform::Transform; -use crate::uuid::{generate_uuid, ManipulatorGroupId}; +use crate::uuid::generate_uuid; +use crate::vector::PointId; use crate::{vector::VectorData, Artboard, Color, GraphicElement, GraphicGroup}; pub use quad::Quad; @@ -14,7 +15,7 @@ use glam::{DAffine2, DVec2}; /// Represents a clickable target for the layer #[derive(Clone, Debug)] pub struct ClickTarget { - pub subpath: bezier_rs::Subpath, + pub subpath: bezier_rs::Subpath, pub stroke_width: f64, } @@ -296,7 +297,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_bezier_paths() { + let _ = subpath.subpath_to_svg(&mut path, multiplied_transform); + } + for subpath in self.stroke_bezier_paths() { let _ = subpath.subpath_to_svg(&mut path, multiplied_transform); } @@ -326,11 +330,8 @@ impl GraphicElementRendered for VectorData { fn add_click_targets(&self, click_targets: &mut Vec) { let stroke_width = self.style.stroke().as_ref().map_or(0., crate::vector::style::Stroke::weight); - let update_closed = |mut subpath: bezier_rs::Subpath| { - subpath.set_closed(self.style.fill().is_some()); - subpath - }; - click_targets.extend(self.subpaths.iter().cloned().map(update_closed).map(|subpath| ClickTarget { stroke_width, subpath })) + click_targets.extend(self.region_bezier_paths().map(|(_, subpath)| ClickTarget { stroke_width, subpath })); + click_targets.extend(self.stroke_bezier_paths().map(|subpath| ClickTarget { stroke_width, subpath })); } fn to_usvg_node(&self) -> usvg::Node { @@ -340,7 +341,7 @@ impl GraphicElementRendered for VectorData { let vector_data = self; let transform = to_transform(vector_data.transform); - for subpath in vector_data.subpaths.iter() { + for subpath in vector_data.stroke_bezier_paths() { let start = vector_data.transform.transform_point2(subpath[0].anchor); builder.move_to(start.x as f32, start.y as f32); for bezier in subpath.iter() { diff --git a/node-graph/gcore/src/uuid.rs b/node-graph/gcore/src/uuid.rs index b18a20be6b..5628adcd0d 100644 --- a/node-graph/gcore/src/uuid.rs +++ b/node-graph/gcore/src/uuid.rs @@ -89,4 +89,14 @@ impl ManipulatorGroupId { self.0 += 1; Self(old) } + + pub(crate) fn inner(self) -> u64 { + self.0 + } +} + +impl From for ManipulatorGroupId { + fn from(value: crate::vector::PointId) -> Self { + Self(value.inner()) + } } diff --git a/node-graph/gcore/src/vector/generator_nodes.rs b/node-graph/gcore/src/vector/generator_nodes.rs index 7144b76c97..bc84c42b39 100644 --- a/node-graph/gcore/src/vector/generator_nodes.rs +++ b/node-graph/gcore/src/vector/generator_nodes.rs @@ -43,7 +43,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 +83,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 +93,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 631cadbc43..f3dd461e57 100644 --- a/node-graph/gcore/src/vector/vector_data.rs +++ b/node-graph/gcore/src/vector/vector_data.rs @@ -1,6 +1,9 @@ +mod attributes; + use super::style::{PathStyle, Stroke}; use crate::Color; use crate::{uuid::ManipulatorGroupId, AlphaBlending}; +pub use attributes::*; use bezier_rs::ManipulatorGroup; use dyn_any::{DynAny, StaticType}; @@ -12,18 +15,23 @@ use glam::{DAffine2, DVec2}; #[derive(Clone, Debug, PartialEq, DynAny)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct VectorData { - pub subpaths: Vec>, pub transform: DAffine2, pub style: PathStyle, pub alpha_blending: AlphaBlending, /// 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 { fn hash(&self, state: &mut H) { - self.subpaths.hash(state); + self.point_domain.hash(state); + self.segment_domain.hash(state); + self.region_domain.hash(state); self.transform.to_cols_array().iter().for_each(|x| x.to_bits().hash(state)); self.style.hash(state); self.alpha_blending.hash(state); @@ -35,31 +43,63 @@ impl VectorData { /// An empty subpath with no data, an identity transform, and a black fill. pub const fn empty() -> Self { Self { - subpaths: Vec::new(), transform: DAffine2::IDENTITY, 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()) + /// 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([subpath]) } - pub fn manipulator_from_id(&self, id: ManipulatorGroupId) -> Option<&ManipulatorGroup> { - self.subpaths.iter().find_map(|subpath| subpath.manipulator_from_id(id)) - } + /// Push a subpath to the vector data + pub fn append_subpath + Copy>(&mut self, subpath: bezier_rs::Subpath) { + for point in subpath.manipulator_groups() { + self.point_domain.push(point.id.into(), point.anchor); + } - /// 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]) + 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 pair in subpath.manipulator_groups().windows(2) { + let id = SegmentId::generate(); + first_seg = Some(first_seg.unwrap_or(id)); + last_seg = Some(id); + self.segment_domain.push(id, pair[0].id.into(), pair[1].id.into(), handles(&pair[0], &pair[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); + self.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] { + self.region_domain.push(RegionId::generate(), first_seg..=last_seg, FillId::generate()); + } + } } /// 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() { + vector_data.append_subpath(subpath); + } + + vector_data } /// Compute the bounding boxes of the subpaths without any transform @@ -69,9 +109,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_bezier_iter() + .map(|(_, bezier, _, _)| bezier.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 0000000000..88140ee0f9 --- /dev/null +++ b/node-graph/gcore/src/vector/vector_data/attributes.rs @@ -0,0 +1,376 @@ +use dyn_any::{DynAny, StaticType}; + +use glam::{DAffine2, DVec2}; +use std::collections::HashMap; + +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()) + } + + pub fn inner(self) -> u64 { + self.0 + } + } + )* + }; +} + +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 core::hash::Hash for PointDomain { + fn hash(&self, state: &mut H) { + self.id.hash(state); + self.positions.iter().for_each(|pos| pos.to_array().map(|v| v.to_bits()).hash(state)); + } +} + +impl PointDomain { + pub const fn new() -> Self { + Self { + id: Vec::new(), + positions: Vec::new(), + } + } + + 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 resolve_id(&self, id: PointId) -> Option { + self.id.iter().position(|&check_id| check_id == id) + } + + 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))); + } + + fn transform(&mut self, transform: DAffine2) { + for pos in &mut self.positions { + *pos = transform.transform_point2(*pos); + } + } +} + +#[derive(Clone, Debug, Default, PartialEq, Hash, 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 end 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, + // TODO: Also store handle points as `PointId`s rather than Bezier-rs's internal `DVec2`s + 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(), + } + } + + 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_id(&self, id: SegmentId) -> Option { + self.ids.iter().position(|&check_id| check_id == id) + } + + 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); + } + + fn transform(&mut self, transform: DAffine2) { + for handles in &mut self.handles { + *handles = handles.apply_transformation(|p| transform.transform_point2(p)); + } + } +} + +#[derive(Clone, Debug, Default, PartialEq, Hash, DynAny)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +/// Stores data which is per-region. A region is an enclosed 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(), + } + } + + 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 resolve_id(&self, id: RegionId) -> Option { + self.ids.iter().position(|&check_id| check_id == id) + } + + 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_bezier(&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_bezier(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_bezier_iter(&self) -> impl Iterator + '_ { + let to_bezier = |(((&handles, &id), &start), &end)| self.segment_to_bezier(start, end, handles).map(|bezier| (id, bezier, 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 continuous. + 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 continuous"); + 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_bezier_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_bezier_paths(&self) -> StrokePathIter<'_> { + StrokePathIter { vector_data: self, segment_index: 0 } + } + + /// Transforms this vector data + pub fn transform(&mut self, transform: DAffine2) { + self.point_domain.transform(transform); + self.segment_domain.transform(transform); + } +} + +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 continuous = old_end.is_none() || old_end.is_some_and(|old_end| old_end == start); + old_end = Some(end); + count += 1; + continuous + }); + + 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::generate() + } +} +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 d252ba1292..5a17127457 100644 --- a/node-graph/gcore/src/vector/vector_nodes.rs +++ b/node-graph/gcore/src/vector/vector_nodes.rs @@ -1,5 +1,5 @@ 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}; @@ -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 { - // 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(); +fn repeat_vector_data(vector_data: VectorData, direction: DVec2, count: u32) -> VectorData { + // Repeat the vector data + 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>>>( +async fn sample_points, FL: Future>>( footprint: Footprint, mut vector_data: impl Node, spacing: f64, @@ -262,18 +239,23 @@ async fn sample_points, FL: Future, ) -> VectorData { - let mut vector_data = self.vector_data.eval(footprint).await; + let vector_data = self.vector_data.eval(footprint).await; let lengths_of_segments_of_subpaths = self.lengths_of_segments_of_subpaths.eval(footprint).await; - for (index, subpath) in &mut vector_data.subpaths.iter_mut().enumerate() { - if subpath.is_empty() || !spacing.is_finite() || spacing <= 0. { - continue; - } + let mut bezier = vector_data.segment_bezier_iter().enumerate().peekable(); - subpath.apply_transform(vector_data.transform); + let mut result = VectorData::empty(); + result.transform = vector_data.transform; + + while let Some((index, (segment, _, _, mut last_end))) = bezier.next() { + let mut lengths = vec![(segment, lengths_of_segments_of_subpaths.get(index).copied().unwrap_or_default())]; + + while let Some((index, (segment, _, _, end))) = bezier.peek().is_some_and(|(_, (_, _, start, _))| *start == last_end).then(|| bezier.next()).flatten() { + last_end = end; + lengths.push((segment, lengths_of_segments_of_subpaths.get(index).copied().unwrap_or_default())); + } - let segment_lengths = &lengths_of_segments_of_subpaths[index]; - let total_length: f64 = segment_lengths.iter().sum(); + let total_length: f64 = lengths.iter().map(|(_, len)| *len).sum(); let mut used_length = total_length - start_offset - stop_offset; if used_length <= 0. { @@ -282,35 +264,43 @@ async fn sample_points, FL: Future= 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 `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. + if count < 1. { + continue; + } + for c in 0..=count as usize { + let fraction = c as f64 / count; + let total_distance = fraction * used_length + start_offset; + + let (mut segment, mut length) = lengths[0]; + let mut total_length_before = 0.; + for &(next_segment, next_length) in lengths.iter().skip(1) { + if total_length_before + length > total_distance { + break; + } - let t = (ratio * used_length + start_offset) / total_length; + total_length_before += length; + segment = next_segment; + length = next_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)) - }); + let Some(segment) = vector_data.segment_from_id(segment) else { continue }; + let segment = segment.apply_transformation(|point| vector_data.transform.transform_point2(point)); - *subpath = Subpath::from_anchors(new_anchors, subpath.closed() && count as usize > 1); + let parametric_t = segment.euclidean_to_parametric_with_total_length((total_distance - total_length_before) / length, 0.001, length); + let point = segment.evaluate(TValue::Parametric(parametric_t)); + result.point_domain.push(PointId::generate(), vector_data.transform.inverse().transform_point2(point)); } - - subpath.apply_transform(vector_data.transform.inverse()); } - vector_data + + result } #[derive(Debug, Clone, Copy)] @@ -319,36 +309,32 @@ pub struct PoissonDiskPoints { } #[node_macro::node_fn(PoissonDiskPoints)] -fn poisson_disk_points(mut vector_data: VectorData, separation_disk_diameter: f64) -> VectorData { +fn poisson_disk_points(vector_data: VectorData, separation_disk_diameter: f64) -> VectorData { let mut rng = rand::rngs::StdRng::seed_from_u64(0); - for subpath in &mut vector_data.subpaths.iter_mut() { + let mut result = VectorData::empty(); + for (_, mut subpath) in vector_data.region_bezier_paths() { if subpath.manipulator_groups().len() < 3 { continue; } subpath.apply_transform(vector_data.transform); - let points = subpath.poisson_disk_points(separation_disk_diameter, || rng.gen::()).into_iter(); - *subpath = Subpath::from_anchors(points, false); - - subpath.apply_transform(vector_data.transform.inverse()); + for point in subpath.poisson_disk_points(separation_disk_diameter, || rng.gen::()) { + result.point_domain.push(PointId::generate(), vector_data.transform.inverse().transform_point2(point)); + } } - vector_data + result } #[derive(Debug, Clone, Copy)] pub struct LengthsOfSegmentsOfSubpaths; #[node_macro::node_fn(LengthsOfSegmentsOfSubpaths)] -fn lengths_of_segments_of_subpaths(mut vector_data: VectorData) -> Vec> { +fn lengths_of_segments_of_subpaths(vector_data: VectorData) -> Vec { vector_data - .subpaths - .iter_mut() - .map(|subpath| { - subpath.apply_transform(vector_data.transform); - subpath.iter().map(|bezier| bezier.length(None)).collect() - }) + .segment_bezier_iter() + .map(|(_id, bezier, _, _)| bezier.apply_transformation(|point| vector_data.transform.transform_point2(point)).length(None)) .collect() } @@ -357,15 +343,20 @@ pub struct SplinesFromPointsNode; #[node_macro::node_fn(SplinesFromPointsNode)] fn splines_from_points(mut vector_data: VectorData) -> VectorData { - for subpath in &mut vector_data.subpaths { - let mut spline = Subpath::new_cubic_spline(subpath.anchors()); + let points = &vector_data.point_domain; - // Preserve the manipulator group ids - for (spline_manipulator_group, original_manipulator_group) in spline.manipulator_groups_mut().iter_mut().zip(subpath.manipulator_groups()) { - spline_manipulator_group.id = original_manipulator_group.id; - } + vector_data.segment_domain.clear(); - *subpath = spline; + let first_handles = bezier_rs::solve_spline_first_handle(points.positions()); + + for (start_index, end_index) in (0..(points.positions().len())).zip(1..(points.positions().len())) { + let handle_start = first_handles[start_index]; + let handle_end = points.positions()[end_index] * 2. - first_handles[end_index]; + let handles = bezier_rs::BezierHandles::Cubic { handle_start, handle_end }; + + vector_data + .segment_domain + .push(SegmentId::generate(), points.ids()[start_index], points.ids()[end_index], handles, StrokeId::generate()) } vector_data @@ -386,13 +377,17 @@ async fn morph, TargetFuture: Future VectorData { - let mut source = self.source.eval(footprint).await; - let mut target = self.target.eval(footprint).await; + let source = self.source.eval(footprint).await; + let target = self.target.eval(footprint).await; + let mut result = VectorData::empty(); // Lerp styles - let style = source.style.lerp(&target.style, time); + result.alpha_blending = if time < 0.5 { source.alpha_blending } else { target.alpha_blending }; + result.style = source.style.lerp(&target.style, time); - for (source_path, target_path) in source.subpaths.iter_mut().zip(target.subpaths.iter_mut()) { + let mut source_paths = source.stroke_bezier_paths(); + let mut target_paths = target.stroke_bezier_paths(); + for (mut source_path, mut target_path) in (&mut source_paths).zip(&mut target_paths) { // Deal with mistmatched transforms source_path.apply_transform(source.transform); target_path.apply_transform(target.transform); @@ -430,38 +425,198 @@ async fn morph, TargetFuture: Future(Node); + + impl<'i, T: 'i, N: Node<'i, T> + Clone> Node<'i, T> for FutureWrapperNode + where + N: Node<'i, T>, + { + type Output = Pin + 'i>>; + fn eval(&'i self, input: T) -> Self::Output { + Box::pin(async move { self.0.eval(input) }) } } - // Create result - let subpaths = std::mem::take(&mut source.subpaths); - let mut current = if time < 0.5 { source } else { target }; - current.style = style; - current.subpaths = subpaths; - current.transform = DAffine2::IDENTITY; - - current + #[test] + fn repeat() { + let direction = DVec2::X * 1.5; + let repeated = RepeatNode { + direction: ClonedNode::new(direction), + count: ClonedNode::new(3), + } + .eval(VectorData::from_subpath(Subpath::new_rect(DVec2::ZERO, DVec2::ONE))); + assert_eq!(repeated.region_bezier_paths().count(), 3); + for (index, (_, subpath)) in repeated.region_bezier_paths().enumerate() { + assert_eq!(subpath.manipulator_groups()[0].anchor, direction * index as f64); + } + } + #[test] + fn circle_repeat() { + let repeated = CircularRepeatNode { + angle_offset: ClonedNode::new(45.), + radius: ClonedNode::new(4.), + count: ClonedNode::new(8), + } + .eval(VectorData::from_subpath(Subpath::new_rect(DVec2::NEG_ONE, DVec2::ONE))); + assert_eq!(repeated.region_bezier_paths().count(), 8); + for (index, (_, subpath)) in repeated.region_bezier_paths().enumerate() { + let expected_angle = (index as f64 + 1.) * 45.; + let centre = (subpath.manipulator_groups()[0].anchor + subpath.manipulator_groups()[2].anchor) / 2.; + let actual_angle = DVec2::Y.angle_between(centre).to_degrees(); + assert!((actual_angle - expected_angle).abs() % 360. < 1e-5); + } + } + #[test] + fn bounding_box() { + let bouding_box = BoundingBoxNode.eval(VectorData::from_subpath(Subpath::new_rect(DVec2::NEG_ONE, DVec2::ONE))); + assert_eq!(bouding_box.region_bezier_paths().count(), 1); + let subpath = bouding_box.region_bezier_paths().next().unwrap().1; + assert_eq!(&subpath.anchors()[..4], &[DVec2::NEG_ONE, DVec2::new(1., -1.), DVec2::ONE, DVec2::new(-1., 1.),]); + } + #[tokio::test] + async fn copy_to_points() { + let points = VectorData::from_subpath(Subpath::new_rect(DVec2::NEG_ONE * 10., DVec2::ONE * 10.)); + let expected_points = points.point_domain.positions().to_vec(); + let bouding_box = CopyToPoints { + points: CullNode::new(FutureWrapperNode(ClonedNode(points))), + instance: CullNode::new(FutureWrapperNode(ClonedNode(VectorData::from_subpath(Subpath::new_rect(DVec2::NEG_ONE, DVec2::ONE))))), + random_scale_min: FutureWrapperNode(ClonedNode(1.)), + random_scale_max: FutureWrapperNode(ClonedNode(1.)), + random_scale_bias: FutureWrapperNode(ClonedNode(0.)), + random_rotation: FutureWrapperNode(ClonedNode(0.)), + } + .eval(Footprint::default()) + .await; + assert_eq!(bouding_box.region_bezier_paths().count(), expected_points.len()); + for (index, (_, subpath)) in bouding_box.region_bezier_paths().enumerate() { + let offset = expected_points[index]; + assert_eq!( + &subpath.anchors()[..4], + &[offset + DVec2::NEG_ONE, offset + DVec2::new(1., -1.), offset + DVec2::ONE, offset + DVec2::new(-1., 1.),] + ); + } + } + #[tokio::test] + async fn sample_points() { + let path = VectorData::from_subpath(Subpath::from_bezier(&Bezier::from_cubic_dvec2(DVec2::ZERO, DVec2::ZERO, DVec2::X * 100., DVec2::X * 100.))); + let sample_points = SamplePoints { + vector_data: CullNode::new(FutureWrapperNode(ClonedNode(path))), + spacing: FutureWrapperNode(ClonedNode(30.)), + start_offset: FutureWrapperNode(ClonedNode(0.)), + stop_offset: FutureWrapperNode(ClonedNode(0.)), + adaptive_spacing: FutureWrapperNode(ClonedNode(false)), + lengths_of_segments_of_subpaths: CullNode::new(FutureWrapperNode(ClonedNode(vec![100.]))), + } + .eval(Footprint::default()) + .await; + assert_eq!(sample_points.point_domain.positions().len(), 4); + for (pos, expected) in sample_points.point_domain.positions().iter().zip([DVec2::X * 0., DVec2::X * 30., DVec2::X * 60., DVec2::X * 90.]) { + assert!(pos.distance(expected) < 1e-3, "Expected {expected} found {pos}"); + } + } + #[tokio::test] + async fn adaptive_spacing() { + let path = VectorData::from_subpath(Subpath::from_bezier(&Bezier::from_cubic_dvec2(DVec2::ZERO, DVec2::ZERO, DVec2::X * 100., DVec2::X * 100.))); + let sample_points = SamplePoints { + vector_data: CullNode::new(FutureWrapperNode(ClonedNode(path))), + spacing: FutureWrapperNode(ClonedNode(18.)), + start_offset: FutureWrapperNode(ClonedNode(45.)), + stop_offset: FutureWrapperNode(ClonedNode(10.)), + adaptive_spacing: FutureWrapperNode(ClonedNode(true)), + lengths_of_segments_of_subpaths: CullNode::new(FutureWrapperNode(ClonedNode(vec![100.]))), + } + .eval(Footprint::default()) + .await; + assert_eq!(sample_points.point_domain.positions().len(), 4); + for (pos, expected) in sample_points.point_domain.positions().iter().zip([DVec2::X * 45., DVec2::X * 60., DVec2::X * 75., DVec2::X * 90.]) { + assert!(pos.distance(expected) < 1e-3, "Expected {expected} found {pos}"); + } + } + #[test] + fn poisson() { + let sample_points = PoissonDiskPoints { + separation_disk_diameter: ClonedNode(10. * std::f64::consts::SQRT_2), + } + .eval(VectorData::from_subpath(Subpath::new_ellipse(DVec2::NEG_ONE * 50., DVec2::ONE * 50.))); + assert!( + (20..=40).contains(&sample_points.point_domain.positions().len()), + "actual len {}", + sample_points.point_domain.positions().len() + ); + for point in sample_points.point_domain.positions() { + assert!(point.length() < 50. + 1., "Expected point in circle {point}") + } + } + #[test] + fn lengths() { + let subpath = VectorData::from_subpath(Subpath::from_bezier(&Bezier::from_cubic_dvec2(DVec2::ZERO, DVec2::ZERO, DVec2::X * 100., DVec2::X * 100.))); + let lengths = LengthsOfSegmentsOfSubpaths.eval(subpath); + assert_eq!(lengths, vec![100.]); + } + #[test] + fn spline() { + let subpath = VectorData::from_subpath(Subpath::new_rect(DVec2::ZERO, DVec2::ONE * 100.)); + let spline = SplinesFromPointsNode.eval(subpath); + assert_eq!(spline.stroke_bezier_paths().count(), 1); + assert_eq!(spline.point_domain.positions(), &[DVec2::ZERO, DVec2::new(100., 0.), DVec2::new(100., 100.), DVec2::new(0., 100.)]); + } + #[tokio::test] + async fn morph() { + let source = VectorData::from_subpath(Subpath::new_rect(DVec2::ZERO, DVec2::ONE * 100.)); + let target = VectorData::from_subpath(Subpath::new_ellipse(DVec2::NEG_ONE * 100., DVec2::ZERO)); + let sample_points = MorphNode { + source: CullNode::new(FutureWrapperNode(ClonedNode(source))), + target: CullNode::new(FutureWrapperNode(ClonedNode(target))), + time: FutureWrapperNode(ClonedNode(0.5)), + start_index: FutureWrapperNode(ClonedNode(0)), + } + .eval(Footprint::default()) + .await; + assert_eq!( + &sample_points.point_domain.positions()[..4], + vec![DVec2::new(-25., -50.), DVec2::new(50., -25.), DVec2::new(25., 50.), DVec2::new(-50., 25.)] + ); + } } diff --git a/node-graph/gstd/src/brush.rs b/node-graph/gstd/src/brush.rs index 520a60c8e2..d8ac5664ec 100644 --- a/node-graph/gstd/src/brush.rs +++ b/node-graph/gstd/src/brush.rs @@ -60,7 +60,7 @@ pub struct VectorPointsNode; #[node_fn(VectorPointsNode)] fn vector_points(vector: VectorData) -> Vec { - vector.subpaths.iter().flat_map(|subpath| subpath.manipulator_groups().iter().map(|group| group.anchor)).collect() + vector.point_domain.positions().to_vec() } #[derive(Clone, Copy, Debug, PartialEq)] diff --git a/node-graph/interpreted-executor/src/node_registry.rs b/node-graph/interpreted-executor/src/node_registry.rs index 2ee4a96350..ecc7b08da5 100644 --- a/node-graph/interpreted-executor/src/node_registry.rs +++ b/node-graph/interpreted-executor/src/node_registry.rs @@ -749,7 +749,7 @@ fn node_registry() -> HashMap, input: Footprint, output: VectorData, fn_params: [Footprint => VectorData, Footprint => VectorData, () => f64, () => f64, () => f64, () => f64]), async_node!(graphene_core::vector::CopyToPoints<_, _, _, _, _, _>, input: Footprint, output: GraphicGroup, fn_params: [Footprint => VectorData, Footprint => GraphicGroup, () => f64, () => f64, () => f64, () => f64]), - async_node!(graphene_core::vector::SamplePoints<_, _, _, _, _, _>, input: Footprint, output: VectorData, fn_params: [Footprint => VectorData, () => f64, () => f64, () => f64, () => bool, Footprint => Vec>]), + async_node!(graphene_core::vector::SamplePoints<_, _, _, _, _, _>, input: Footprint, output: VectorData, fn_params: [Footprint => VectorData, () => f64, () => f64, () => f64, () => bool, Footprint => Vec]), register_node!(graphene_core::vector::PoissonDiskPoints<_>, input: VectorData, params: [f64]), register_node!(graphene_core::vector::LengthsOfSegmentsOfSubpaths, input: VectorData, params: []), register_node!(graphene_core::vector::SplinesFromPointsNode, input: VectorData, params: []), diff --git a/website/other/bezier-rs-demos/wasm/src/bezier.rs b/website/other/bezier-rs-demos/wasm/src/bezier.rs index 3d87909190..cccde1bada 100644 --- a/website/other/bezier-rs-demos/wasm/src/bezier.rs +++ b/website/other/bezier-rs-demos/wasm/src/bezier.rs @@ -164,7 +164,7 @@ impl WasmBezier { "Euclidean" => TValueType::Euclidean, _ => panic!("Unexpected TValue string: '{t_variant}'"), }; - let table_values: Vec = self.0.compute_lookup_table(Some(steps), Some(tvalue_type)); + let table_values: Vec = self.0.compute_lookup_table(Some(steps), Some(tvalue_type)).collect(); let circles: String = table_values .iter() .map(|point| draw_circle(*point, 3., RED, 1.5, WHITE)) @@ -293,7 +293,7 @@ impl WasmBezier { } pub fn project(&self, x: f64, y: f64) -> String { - let projected_t_value = self.0.project(DVec2::new(x, y), None); + let projected_t_value = self.0.project(DVec2::new(x, y)); let projected_point = self.0.evaluate(TValue::Parametric(projected_t_value)); let bezier = self.get_bezier_path(); diff --git a/website/other/bezier-rs-demos/wasm/src/subpath.rs b/website/other/bezier-rs-demos/wasm/src/subpath.rs index c485f23af0..b115e6853b 100644 --- a/website/other/bezier-rs-demos/wasm/src/subpath.rs +++ b/website/other/bezier-rs-demos/wasm/src/subpath.rs @@ -236,7 +236,7 @@ impl WasmSubpath { } pub fn project(&self, x: f64, y: f64) -> String { - let (segment_index, projected_t) = self.0.project(DVec2::new(x, y), None).unwrap(); + let (segment_index, projected_t) = self.0.project(DVec2::new(x, y)).unwrap(); let projected_point = self.0.evaluate(SubpathTValue::Parametric { segment_index, t: projected_t }); let subpath_svg = self.to_default_svg();