New node: String Split (#3304)

* Add two nodes for substring

* fix: number convention

* Remove unnecessay allocations and add support for \n in patterns

* Make split node return Vec<String> and remove count_elements node

* Add Cache implementations for Vec<String>

* Code review

---------

Co-authored-by: daniil.loban <daniil.loban@gmail.com>
Co-authored-by: Dennis Kobert <dennis@kobert.dev>
Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
Daniil Loban 2025-11-04 00:02:12 +03:00 committed by GitHub
parent c4bbb7e880
commit 0d8c521fbe
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 54 additions and 3 deletions

View File

@ -31,6 +31,7 @@ async fn context_modification<T>(
Context -> Vec<NodeId>, Context -> Vec<NodeId>,
Context -> Vec<f64>, Context -> Vec<f64>,
Context -> Vec<f32>, Context -> Vec<f32>,
Context -> Vec<String>,
Context -> Table<Vector>, Context -> Table<Vector>,
Context -> Table<Graphic>, Context -> Table<Graphic>,
Context -> Table<Raster<CPU>>, Context -> Table<Raster<CPU>>,

View File

@ -451,6 +451,7 @@ fn index<T: AtIndex + Clone + Default>(
Vec<u32>, Vec<u32>,
Vec<u64>, Vec<u64>,
Vec<DVec2>, Vec<DVec2>,
Vec<String>,
Table<Artboard>, Table<Artboard>,
Table<Graphic>, Table<Graphic>,
Table<Vector>, Table<Vector>,

View File

@ -69,6 +69,17 @@ fn string_length(_: impl Ctx, string: String) -> f64 {
string.chars().count() as f64 string.chars().count() as f64
} }
#[node_macro::node(category("Text"))]
fn string_split(_: impl Ctx, string: String, #[default("\\n")] delimeter: String, #[default(true)] delimeter_escaping: bool) -> Vec<String> {
let delimeter = if delimeter_escaping {
delimeter.replace("\\n", "\n").replace("\\r", "\r").replace("\\t", "\t").replace("\\0", "\0").replace("\\\\", "\\")
} else {
delimeter
};
string.split(&delimeter).map(str::to_string).collect()
}
/// Evaluates either the "If True" or "If False" input branch based on whether the input condition is true or false. /// Evaluates either the "If True" or "If False" input branch based on whether the input condition is true or false.
#[node_macro::node(category("Math: Logic"))] #[node_macro::node(category("Math: Logic"))]
async fn switch<T, C: Send + 'n + Clone>( async fn switch<T, C: Send + 'n + Clone>(

View File

@ -1954,11 +1954,39 @@ fn point_inside(_: impl Ctx, source: Table<Vector>, point: DVec2) -> bool {
source.into_iter().any(|row| row.element.check_point_inside_shape(row.transform, point)) source.into_iter().any(|row| row.element.check_point_inside_shape(row.transform, point))
} }
trait Count {
fn count(&self) -> usize;
}
impl<T> Count for Table<T> {
fn count(&self) -> usize {
self.len()
}
}
impl<T> Count for Vec<T> {
fn count(&self) -> usize {
self.len()
}
}
// TODO: Return u32, u64, or usize instead of f64 after #1621 is resolved and has allowed us to implement automatic type conversion in the node graph for nodes with generic type inputs. // TODO: Return u32, u64, or usize instead of f64 after #1621 is resolved and has allowed us to implement automatic type conversion in the node graph for nodes with generic type inputs.
// TODO: (Currently automatic type conversion only works for concrete types, via the Graphene preprocessor and not the full Graphene type system.) // TODO: (Currently automatic type conversion only works for concrete types, via the Graphene preprocessor and not the full Graphene type system.)
#[node_macro::node(category("General"), path(graphene_core::vector))] #[node_macro::node(category("General"), path(graphene_core::vector))]
async fn count_elements<I>(_: impl Ctx, #[implementations(Table<Graphic>, Table<Vector>, Table<Raster<CPU>>, Table<Raster<GPU>>, Table<Color>, Table<GradientStops>)] source: Table<I>) -> f64 { async fn count_elements<I: Count>(
source.len() as f64 _: impl Ctx,
#[implementations(
Table<Graphic>,
Table<Vector>,
Table<Raster<CPU>>,
Table<Raster<GPU>>,
Table<Color>,
Table<GradientStops>,
Vec<String>,
Vec<f64>,
Vec<DVec2>,
)]
source: I,
) -> f64 {
source.count() as f64
} }
#[node_macro::node(category("Vector: Measure"), path(graphene_core::vector))] #[node_macro::node(category("Vector: Measure"), path(graphene_core::vector))]

View File

@ -108,6 +108,7 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::PointSpacingType]), async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::PointSpacingType]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Option<f64>]), async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Option<f64>]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Vec<DVec2>]), async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Vec<DVec2>]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Vec<String>]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => [f64; 4]]), async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => [f64; 4]]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Vec<NodeId>]), async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Vec<NodeId>]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Graphic]), async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Graphic]),
@ -165,6 +166,7 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Vec<NodeId>]), async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Vec<NodeId>]),
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Vec<f64>]), async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Vec<f64>]),
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Vec<f32>]), async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Vec<f32>]),
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Vec<String>]),
#[cfg(feature = "gpu")] #[cfg(feature = "gpu")]
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Arc<WasmSurfaceHandle>]), async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Arc<WasmSurfaceHandle>]),
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => WindowHandle]), async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => WindowHandle]),

View File

@ -88,7 +88,15 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
.iter() .iter()
.map(|field| match &field.ty { .map(|field| match &field.ty {
ParsedFieldType::Regular(RegularParsedField { value_source, .. }) => match value_source { ParsedFieldType::Regular(RegularParsedField { value_source, .. }) => match value_source {
ParsedValueSource::Default(data) => quote!(RegistryValueSource::Default(stringify!(#data))), ParsedValueSource::Default(data) => {
// Check if the data is a string literal by parsing the token stream
let data_str = data.to_string();
if data_str.starts_with('"') && data_str.ends_with('"') && data_str.len() >= 2 {
quote!(RegistryValueSource::Default(#data))
} else {
quote!(RegistryValueSource::Default(stringify!(#data)))
}
}
ParsedValueSource::Scope(data) => quote!(RegistryValueSource::Scope(#data)), ParsedValueSource::Scope(data) => quote!(RegistryValueSource::Scope(#data)),
_ => quote!(RegistryValueSource::None), _ => quote!(RegistryValueSource::None),
}, },