mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2025-07-07 15:55:00 +00:00
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:
parent
b5975e92b2
commit
930278128d
2 changed files with 145 additions and 3 deletions
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue