From 930278128d1af2d29f859e56f9076e2b00cd6616 Mon Sep 17 00:00:00 2001 From: Priyanshu Date: Mon, 23 Jun 2025 10:29:25 +0530 Subject: [PATCH] New nodes: 'Path Length', 'Count', and 'Split Path' (#2731) * impl path length node * test 'Path Length' node implementation * improve test * impl 'Count' node to return the number of instance in a VectorDataTable instances * impl 'Split Path' node * don't split if t is close of 0 or 1 or the bezpath is empty * write comments * preserve the style on vector data in 'Split Path' node implementation * Code review --------- Co-authored-by: Keavon Chambers --- .../vector/algorithms/bezpath_algorithms.rs | 49 +++++++++ node-graph/gcore/src/vector/vector_nodes.rs | 99 ++++++++++++++++++- 2 files changed, 145 insertions(+), 3 deletions(-) diff --git a/node-graph/gcore/src/vector/algorithms/bezpath_algorithms.rs b/node-graph/gcore/src/vector/algorithms/bezpath_algorithms.rs index 7dd6f11c4..fa7682e63 100644 --- a/node-graph/gcore/src/vector/algorithms/bezpath_algorithms.rs +++ b/node-graph/gcore/src/vector/algorithms/bezpath_algorithms.rs @@ -3,6 +3,55 @@ use crate::vector::misc::dvec2_to_point; use glam::DVec2; use kurbo::{BezPath, DEFAULT_ACCURACY, Line, ParamCurve, ParamCurveDeriv, PathEl, PathSeg, Point, Rect, Shape}; +/// Splits the [`BezPath`] at `t` value which lie in the range of [0, 1]. +/// Returns [`None`] if the given [`BezPath`] has no segments or `t` is within f64::EPSILON of 0 or 1. +pub fn split_bezpath(bezpath: &BezPath, t: f64, euclidian: bool) -> Option<(BezPath, BezPath)> { + if t <= f64::EPSILON || (1. - t) <= f64::EPSILON || bezpath.segments().count() == 0 { + return None; + } + + // Get the segment which lies at the split. + let (segment_index, t) = t_value_to_parametric(bezpath, t, euclidian, None); + let segment = bezpath.get_seg(segment_index + 1).unwrap(); + + // Divide the segment. + let first_segment = segment.subsegment(0.0..t); + let second_segment = segment.subsegment(t..1.); + + let mut first_bezpath = BezPath::new(); + let mut second_bezpath = BezPath::new(); + + // Append the segments up to the subdividing segment from original bezpath to first bezpath. + for segment in bezpath.segments().take(segment_index) { + if first_bezpath.elements().is_empty() { + first_bezpath.move_to(segment.start()); + } + first_bezpath.push(segment.as_path_el()); + } + + // Append the first segment of the subdivided segment. + if first_bezpath.elements().is_empty() { + first_bezpath.move_to(first_segment.start()); + } + first_bezpath.push(first_segment.as_path_el()); + + // Append the second segment of the subdivided segment in the second bezpath. + if second_bezpath.elements().is_empty() { + second_bezpath.move_to(second_segment.start()); + } + second_bezpath.push(second_segment.as_path_el()); + + // Append the segments after the subdividing segment from original bezpath to second bezpath. + for segment in bezpath.segments().skip(segment_index + 1) { + if second_bezpath.elements().is_empty() { + second_bezpath.move_to(segment.start()); + } + second_bezpath.push(segment.as_path_el()); + } + + Some((first_bezpath, second_bezpath)) +} + pub fn position_on_bezpath(bezpath: &BezPath, t: f64, euclidian: bool, segments_length: Option<&[f64]>) -> Point { let (segment_index, t) = t_value_to_parametric(bezpath, t, euclidian, segments_length); bezpath.get_seg(segment_index + 1).unwrap().eval(t) diff --git a/node-graph/gcore/src/vector/vector_nodes.rs b/node-graph/gcore/src/vector/vector_nodes.rs index 6d710c256..c8030b8f4 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::algorithms::bezpath_algorithms::{self, position_on_bezpath, sample_points_on_bezpath, tangent_on_bezpath}; +use super::algorithms::bezpath_algorithms::{self, position_on_bezpath, sample_points_on_bezpath, split_bezpath, tangent_on_bezpath}; use super::algorithms::offset_subpath::offset_subpath; use super::algorithms::spline::{solve_spline_first_handle_closed, solve_spline_first_handle_open}; use super::misc::{CentroidType, point_to_dvec2}; use super::style::{Fill, Gradient, GradientStops, Stroke}; use super::{PointId, SegmentDomain, SegmentId, StrokeId, VectorData, VectorDataTable}; use crate::instances::{Instance, InstanceMut, Instances}; -use crate::raster_types::{CPU, RasterDataTable}; +use crate::raster_types::{CPU, GPU, RasterDataTable}; use crate::registry::types::{Angle, Fraction, IntegerCount, Length, Multiplier, Percentage, PixelLength, PixelSize, SeedValue}; use crate::renderer::GraphicElementRendered; use crate::transform::{Footprint, ReferencePoint, Transform}; @@ -1314,6 +1314,45 @@ async fn sample_points(_: impl Ctx, vector_data: VectorDataTable, spacing: f64, result_table } +#[node_macro::node(category("Vector"), path(graphene_core::vector))] +async fn split_path(_: impl Ctx, mut vector_data: VectorDataTable, t_value: f64, parameterized_distance: bool, reverse: bool) -> VectorDataTable { + let euclidian = !parameterized_distance; + + let bezpaths = vector_data + .instance_ref_iter() + .enumerate() + .flat_map(|(instance_row_index, vector_data)| vector_data.instance.stroke_bezpath_iter().map(|bezpath| (instance_row_index, bezpath)).collect::>()) + .collect::>(); + + let bezpath_count = bezpaths.len() as f64; + let t_value = t_value.clamp(0., bezpath_count); + let t_value = if reverse { bezpath_count - t_value } else { t_value }; + let index = if t_value >= bezpath_count { (bezpath_count - 1.) as usize } else { t_value as usize }; + + if let Some((instance_row_index, bezpath)) = bezpaths.get(index).cloned() { + let mut result_vector_data = VectorData { + style: vector_data.get(instance_row_index).unwrap().instance.style.clone(), + ..Default::default() + }; + + for (_, (_, bezpath)) in bezpaths.iter().enumerate().filter(|(i, (ri, _))| *i != index && *ri == instance_row_index) { + result_vector_data.append_bezpath(bezpath.clone()); + } + let t = if t_value == bezpath_count { 1. } else { t_value.fract() }; + + if let Some((first, second)) = split_bezpath(&bezpath, t, euclidian) { + result_vector_data.append_bezpath(first); + result_vector_data.append_bezpath(second); + } else { + result_vector_data.append_bezpath(bezpath); + } + + *vector_data.get_mut(instance_row_index).unwrap().instance = result_vector_data; + } + + vector_data +} + /// Determines the position of a point on the path, given by its progress from 0 to 1 along the path. /// If multiple subpaths make up the path, the whole number part of the progress value selects the subpath and the decimal part determines the position along it. #[node_macro::node(name("Position on Path"), category("Vector"), path(graphene_core::vector))] @@ -1870,6 +1909,29 @@ fn point_inside(_: impl Ctx, source: VectorDataTable, point: DVec2) -> bool { source.instance_iter().any(|instance| instance.instance.check_point_inside_shape(instance.transform, point)) } +#[node_macro::node(category("Vector"), path(graphene_core::vector))] +async fn count_elements(_: impl Ctx, #[implementations(GraphicGroupTable, VectorDataTable, RasterDataTable, RasterDataTable)] source: Instances) -> u64 { + source.instance_iter().count() as u64 +} + +#[node_macro::node(category("Vector"), path(graphene_core::vector))] +async fn path_length(_: impl Ctx, source: VectorDataTable) -> f64 { + source + .instance_iter() + .map(|vector_data_instance| { + let transform = vector_data_instance.transform; + vector_data_instance + .instance + .stroke_bezpath_iter() + .map(|mut bezpath| { + bezpath.apply_affine(Affine::new(transform.to_cols_array())); + bezpath.perimeter(DEFAULT_ACCURACY) + }) + .sum::() + }) + .sum() +} + #[node_macro::node(category("Vector"), path(graphene_core::vector))] async fn area(ctx: impl Ctx + CloneVarArgs + ExtractAll, vector_data: impl Node, Output = VectorDataTable>) -> f64 { let new_ctx = OwnedContextImpl::from(ctx).with_footprint(Footprint::default()).into_context(); @@ -1942,6 +2004,7 @@ mod test { use super::*; use crate::Node; use bezier_rs::Bezier; + use kurbo::Rect; use std::pin::Pin; #[derive(Clone)] @@ -1959,6 +2022,24 @@ mod test { VectorDataTable::new(VectorData::from_subpath(data)) } + fn create_vector_data_instance(bezpath: BezPath, transform: DAffine2) -> Instance { + let mut instance = VectorData::default(); + instance.append_bezpath(bezpath); + Instance { + instance, + transform, + ..Default::default() + } + } + + fn vector_node_from_instances(data: Vec>) -> VectorDataTable { + let mut vector_data_table = VectorDataTable::default(); + for instance in data { + vector_data_table.push(instance); + } + vector_data_table + } + #[tokio::test] async fn repeat() { let direction = DVec2::X * 1.5; @@ -2085,12 +2166,24 @@ mod test { } } #[tokio::test] - async fn lengths() { + async fn segment_lengths() { let subpath = Subpath::from_bezier(&Bezier::from_cubic_dvec2(DVec2::ZERO, DVec2::ZERO, DVec2::X * 100., DVec2::X * 100.)); let lengths = subpath_segment_lengths(Footprint::default(), vector_node(subpath)).await; assert_eq!(lengths, vec![100.]); } #[tokio::test] + async fn path_length() { + let bezpath = Rect::new(100., 100., 201., 201.).to_path(DEFAULT_ACCURACY); + let transform = DAffine2::from_scale(DVec2::new(2., 2.)); + let instance = create_vector_data_instance(bezpath, transform); + let instances = (0..5).map(|_| instance.clone()).collect::>>(); + + let length = super::path_length(Footprint::default(), vector_node_from_instances(instances)).await; + + // 4040 equals 101 * 4 (rectangle perimeter) * 2 (scale) * 5 (number of rows) + assert_eq!(length, 4040.); + } + #[tokio::test] async fn spline() { let spline = super::spline(Footprint::default(), vector_node(Subpath::new_rect(DVec2::ZERO, DVec2::ONE * 100.))).await; let spline = spline.instance_ref_iter().next().unwrap().instance;