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 <keavon@keavon.com>
This commit is contained in:
Priyanshu 2025-06-23 10:29:25 +05:30 committed by GitHub
parent b5975e92b2
commit 930278128d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 145 additions and 3 deletions

View file

@ -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)

View file

@ -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::<Vec<_>>())
.collect::<Vec<_>>();
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<I>(_: impl Ctx, #[implementations(GraphicGroupTable, VectorDataTable, RasterDataTable<CPU>, RasterDataTable<GPU>)] source: Instances<I>) -> 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::<f64>()
})
.sum()
}
#[node_macro::node(category("Vector"), path(graphene_core::vector))]
async fn area(ctx: impl Ctx + CloneVarArgs + ExtractAll, vector_data: impl Node<Context<'static>, 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<VectorData> {
let mut instance = VectorData::default();
instance.append_bezpath(bezpath);
Instance {
instance,
transform,
..Default::default()
}
}
fn vector_node_from_instances(data: Vec<Instance<VectorData>>) -> 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::<Vec<Instance<VectorData>>>();
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;