From 7a28d0c28c5c0c06f72e3121074fb48726b39cb0 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Thu, 23 Apr 2026 17:03:30 -0700 Subject: [PATCH] Move the String category nodes from gcore/src/logic.rs to text/src/lib.rs --- Cargo.lock | 4 +- .../node_graph/document_node_definitions.rs | 2 +- .../document/node_graph/node_properties.rs | 1 - .../messages/portfolio/document_migration.rs | 76 ++++---- node-graph/nodes/gcore/Cargo.toml | 1 - node-graph/nodes/gcore/src/animation.rs | 4 +- node-graph/nodes/gcore/src/lib.rs | 2 - node-graph/nodes/gcore/src/logic.rs | 180 ------------------ node-graph/nodes/gstd/src/lib.rs | 4 - node-graph/nodes/math/Cargo.toml | 1 + node-graph/nodes/math/src/lib.rs | 58 +++++- node-graph/nodes/text/Cargo.toml | 2 + node-graph/nodes/text/src/lib.rs | 143 +++++++++++++- 13 files changed, 240 insertions(+), 238 deletions(-) delete mode 100644 node-graph/nodes/gcore/src/logic.rs diff --git a/Cargo.lock b/Cargo.lock index 33df1cdfc7..2615f275a0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1990,7 +1990,6 @@ dependencies = [ "node-macro", "raster-types", "serde", - "serde_json", "tsify", "wasm-bindgen", ] @@ -3082,6 +3081,7 @@ version = "0.1.0" dependencies = [ "core-types", "glam", + "graphic-types", "log", "math-parser", "node-macro", @@ -5503,7 +5503,9 @@ dependencies = [ "log", "node-macro", "parley", + "raster-types", "serde", + "serde_json", "skrifa 0.40.0", "tsify", "vector-types", diff --git a/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs b/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs index 3ee48af260..9b54c952d5 100644 --- a/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs +++ b/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs @@ -537,7 +537,7 @@ fn document_node_definitions() -> HashMap number_widget(default_info, number_input.mode_range().min(min(0.)).max(max(1.))).into(), Some("Progression") => progression_widget(default_info, number_input.min(min(0.))).into(), Some("SignedInteger") => number_widget(default_info, number_input.int()).into(), - Some("IntegerCount") => number_widget(default_info, number_input.int().min(min(1.))).into(), Some("SeedValue") => number_widget(default_info, number_input.int().min(min(0.))).into(), Some("PixelSize") => vec2_widget(default_info, "X", "Y", unit.unwrap_or(" px"), None, false), Some("TextArea") => text_area_widget(default_info).into(), diff --git a/editor/src/messages/portfolio/document_migration.rs b/editor/src/messages/portfolio/document_migration.rs index 68a18fbe5b..de1f89368c 100644 --- a/editor/src/messages/portfolio/document_migration.rs +++ b/editor/src/messages/portfolio/document_migration.rs @@ -108,10 +108,6 @@ const NODE_REPLACEMENTS: &[NodeReplacement<'static>] = &[ node: graphene_std::animation::real_time::IDENTIFIER, aliases: &["graphene_core::animation::RealTimeNode"], }, - NodeReplacement { - node: graphene_std::logic::serialize::IDENTIFIER, - aliases: &["graphene_core::logic::SerializeNode"], - }, NodeReplacement { node: graphene_std::debug::size_of::IDENTIFIER, aliases: &["graphene_core::ops::SizeOfNode"], @@ -120,34 +116,6 @@ const NODE_REPLACEMENTS: &[NodeReplacement<'static>] = &[ node: graphene_std::debug::some::IDENTIFIER, aliases: &["graphene_core::ops::SomeNode"], }, - NodeReplacement { - node: graphene_std::logic::string_concatenate::IDENTIFIER, - aliases: &["graphene_core::logic::StringConcatenateNode"], - }, - NodeReplacement { - node: graphene_std::logic::string_length::IDENTIFIER, - aliases: &["graphene_core::logic::StringLengthNode"], - }, - NodeReplacement { - node: graphene_std::logic::string_replace::IDENTIFIER, - aliases: &["graphene_core::logic::StringReplaceNode"], - }, - NodeReplacement { - node: graphene_std::logic::string_slice::IDENTIFIER, - aliases: &["graphene_core::logic::StringSliceNode"], - }, - NodeReplacement { - node: graphene_std::logic::string_split::IDENTIFIER, - aliases: &["graphene_core::logic::StringSplitNode"], - }, - NodeReplacement { - node: graphene_std::logic::switch::IDENTIFIER, - aliases: &["graphene_core::logic::SwitchNode"], - }, - NodeReplacement { - node: graphene_std::logic::to_string::IDENTIFIER, - aliases: &["graphene_core::logic::ToStringNode"], - }, NodeReplacement { node: graphene_std::debug::unwrap_option::IDENTIFIER, aliases: &["graphene_core::ops::UnwrapNode", "graphene_core::debug::UnwrapNode"], @@ -416,10 +384,6 @@ const NODE_REPLACEMENTS: &[NodeReplacement<'static>] = &[ node: graphene_std::math_nodes::sine_inverse::IDENTIFIER, aliases: &["graphene_math_nodes::SineInverseNode", "graphene_core::ops::SineInverseNode"], }, - NodeReplacement { - node: graphene_std::math_nodes::string_value::IDENTIFIER, - aliases: &["graphene_math_nodes::StringValueNode", "graphene_core::ops::StringValueNode"], - }, NodeReplacement { node: graphene_std::math_nodes::subtract::IDENTIFIER, aliases: &["graphene_math_nodes::SubtractNode", "graphene_core::ops::SubtractNode"], @@ -680,6 +644,46 @@ const NODE_REPLACEMENTS: &[NodeReplacement<'static>] = &[ node: graphene_std::text::text::IDENTIFIER, aliases: &["graphene_core::text::text::TextNode", "graphene_core::text::TextGeneratorNode", "graphene_core::text::TextNode"], }, + NodeReplacement { + node: graphene_std::text_nodes::string_value::IDENTIFIER, + aliases: &["graphene_math_nodes::StringValueNode", "graphene_core::ops::StringValueNode", "math_nodes::StringValueNode"], + }, + NodeReplacement { + node: graphene_std::text_nodes::string_concatenate::IDENTIFIER, + aliases: &["graphene_core::logic::StringConcatenateNode"], + }, + NodeReplacement { + node: graphene_std::text_nodes::string_length::IDENTIFIER, + aliases: &["graphene_core::logic::StringLengthNode"], + }, + NodeReplacement { + node: graphene_std::text_nodes::string_replace::IDENTIFIER, + aliases: &["graphene_core::logic::StringReplaceNode"], + }, + NodeReplacement { + node: graphene_std::text_nodes::string_slice::IDENTIFIER, + aliases: &["graphene_core::logic::StringSliceNode"], + }, + NodeReplacement { + node: graphene_std::text_nodes::string_split::IDENTIFIER, + aliases: &["graphene_core::logic::StringSplitNode"], + }, + NodeReplacement { + node: graphene_std::math_nodes::switch::IDENTIFIER, + aliases: &["graphene_core::logic::SwitchNode"], + }, + NodeReplacement { + node: graphene_std::text_nodes::to_string::IDENTIFIER, + aliases: &["graphene_core::logic::ToStringNode"], + }, + NodeReplacement { + node: graphene_std::text_nodes::json_get::IDENTIFIER, + aliases: &["graphene_core::logic::JsonGetNode"], + }, + NodeReplacement { + node: graphene_std::text_nodes::serialize::IDENTIFIER, + aliases: &["graphene_core::logic::SerializeNode"], + }, // ================================ // transform // ================================ diff --git a/node-graph/nodes/gcore/Cargo.toml b/node-graph/nodes/gcore/Cargo.toml index 9d2fbabb03..740a021b59 100644 --- a/node-graph/nodes/gcore/Cargo.toml +++ b/node-graph/nodes/gcore/Cargo.toml @@ -27,7 +27,6 @@ node-macro = { workspace = true } dyn-any = { workspace = true } glam = { workspace = true } log = { workspace = true } -serde_json = { workspace = true } # Optional workspace dependencies serde = { workspace = true, optional = true } diff --git a/node-graph/nodes/gcore/src/animation.rs b/node-graph/nodes/gcore/src/animation.rs index 4498737130..50c682d0b3 100644 --- a/node-graph/nodes/gcore/src/animation.rs +++ b/node-graph/nodes/gcore/src/animation.rs @@ -60,7 +60,7 @@ fn animation_time( ctx.try_animation_time().unwrap_or_default() * rate } -#[node_macro::node(category("Animation"))] +#[node_macro::node(category("Debug"))] async fn quantize_real_time( ctx: impl Ctx + ExtractAll + CloneVarArgs, #[implementations( @@ -103,7 +103,7 @@ async fn quantize_real_time( value.eval(Some(new_context.into())).await } -#[node_macro::node(category("Animation"))] +#[node_macro::node(category("Debug"))] async fn quantize_animation_time( ctx: impl Ctx + ExtractAll + CloneVarArgs, #[implementations( diff --git a/node-graph/nodes/gcore/src/lib.rs b/node-graph/nodes/gcore/src/lib.rs index 9d1c0e132b..5f74dd3cd2 100644 --- a/node-graph/nodes/gcore/src/lib.rs +++ b/node-graph/nodes/gcore/src/lib.rs @@ -3,7 +3,6 @@ pub mod context; pub mod context_modification; pub mod debug; pub mod extract_xy; -pub mod logic; pub mod memo; pub mod ops; @@ -13,6 +12,5 @@ pub use context::*; pub use context_modification::*; pub use debug::*; pub use extract_xy::*; -pub use logic::*; pub use memo::*; pub use ops::*; diff --git a/node-graph/nodes/gcore/src/logic.rs b/node-graph/nodes/gcore/src/logic.rs deleted file mode 100644 index 68391d5e01..0000000000 --- a/node-graph/nodes/gcore/src/logic.rs +++ /dev/null @@ -1,180 +0,0 @@ -use core_types::Color; -use core_types::registry::types::TextArea; -use core_types::table::Table; -use core_types::{Context, Ctx}; -use glam::{DAffine2, DVec2}; -use graphic_types::vector_types::GradientStops; -use graphic_types::{Artboard, Graphic, Vector}; -use raster_types::{CPU, GPU, Raster}; - -/// Type-asserts a value to be a string. -#[node_macro::node(category("Debug"))] -fn to_string(_: impl Ctx, value: String) -> String { - value -} - -/// Converts a value to a JSON string representation. -#[node_macro::node(category("Text"))] -fn serialize( - _: impl Ctx, - #[implementations(String, bool, f64, u32, u64, DVec2, DAffine2, /* Table, Table, Table, */ Table>, Table /* , Table */)] value: T, -) -> String { - serde_json::to_string(&value).unwrap_or_else(|_| "Serialization Error".to_string()) -} - -/// Joins two strings together. -#[node_macro::node(category("Text"))] -fn string_concatenate(_: impl Ctx, #[implementations(String)] first: String, second: TextArea) -> String { - first.clone() + &second -} - -/// Replaces all occurrences of "From" with "To" in the input string. -#[node_macro::node(category("Text"))] -fn string_replace(_: impl Ctx, string: String, from: TextArea, to: TextArea) -> String { - string.replace(&from, &to) -} - -/// Extracts a substring from the input string, starting at "Start" and ending before "End". -/// Negative indices count from the end of the string. -/// If "Start" equals or exceeds "End", the result is an empty string. -#[node_macro::node(category("Text"))] -fn string_slice(_: impl Ctx, string: String, start: f64, end: f64) -> String { - let total_chars = string.chars().count(); - - let start = if start < 0. { - total_chars.saturating_sub(start.abs() as usize) - } else { - (start as usize).min(total_chars) - }; - let end = if end <= 0. { - total_chars.saturating_sub(end.abs() as usize) - } else { - (end as usize).min(total_chars) - }; - - if start >= end { - return String::new(); - } - - string.chars().skip(start).take(end - start).collect() -} - -// 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.) -/// Counts the number of characters in a string. -#[node_macro::node(category("Text"))] -fn string_length(_: impl Ctx, string: String) -> f64 { - string.chars().count() as f64 -} - -/// Splits a string into a list of substrings based on the specified delimeter. -/// For example, the delimeter "," will split "a,b,c" into the strings "a", "b", and "c". -#[node_macro::node(category("Text"))] -fn string_split( - _: impl Ctx, - /// The string to split into substrings. - string: String, - /// The character(s) that separate the substrings. These are not included in the outputs. - #[default("\\n")] - delimeter: String, - /// Whether to convert escape sequences found in the delimeter into their corresponding characters: - /// "\n" (newline), "\r" (carriage return), "\t" (tab), "\0" (null), and "\\" (backslash). - #[default(true)] - delimeter_escaping: bool, -) -> Vec { - 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() -} - -/// Gets a value from either a json object or array given as a string input. -/// For example, for the input {"name": "ferris"} the key "name" will return "ferris". -#[node_macro::node(category("Text"))] -fn json_get( - _: impl Ctx, - /// The json data. - data: String, - /// The key to index the object with. - key: String, -) -> String { - use serde_json::Value; - let Ok(value): Result = serde_json::from_str(&data) else { - return "Input is not valid json".into(); - }; - match value { - Value::Array(ref arr) => { - let Ok(index): Result = key.parse() else { - log::error!("Json input is an array, but key is not a number"); - return String::new(); - }; - let Some(value) = arr.get(index) else { - log::error!("Index {} out of bounds for len {}", index, arr.len()); - return String::new(); - }; - value.to_string() - } - Value::Object(map) => { - let Some(value) = map.get(&key) else { - log::error!("Key {key} not found in object"); - return String::new(); - }; - match value { - Value::String(s) => s.clone(), - Value::Number(n) => n.to_string(), - complex => complex.to_string(), - } - } - _ => String::new(), - } -} - -/// 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"))] -async fn switch( - #[implementations(Context)] ctx: C, - condition: bool, - #[expose] - #[implementations( - Context -> String, - Context -> bool, - Context -> f32, - Context -> f64, - Context -> u32, - Context -> u64, - Context -> DVec2, - Context -> DAffine2, - Context -> Table, - Context -> Table, - Context -> Table, - Context -> Table>, - Context -> Table>, - Context -> Table, - Context -> Table, - )] - if_true: impl Node, - #[expose] - #[implementations( - Context -> String, - Context -> bool, - Context -> f32, - Context -> f64, - Context -> u32, - Context -> u64, - Context -> DVec2, - Context -> DAffine2, - Context -> Table, - Context -> Table, - Context -> Table, - Context -> Table>, - Context -> Table>, - Context -> Table, - Context -> Table, - )] - if_false: impl Node, -) -> T { - if condition { if_true.eval(ctx).await } else { if_false.eval(ctx).await } -} diff --git a/node-graph/nodes/gstd/src/lib.rs b/node-graph/nodes/gstd/src/lib.rs index f7d36139d6..7f1f059117 100644 --- a/node-graph/nodes/gstd/src/lib.rs +++ b/node-graph/nodes/gstd/src/lib.rs @@ -78,10 +78,6 @@ pub mod math { } } -pub mod logic { - pub use graphene_core::logic::*; -} - pub mod context { pub use graphene_core::context::*; } diff --git a/node-graph/nodes/math/Cargo.toml b/node-graph/nodes/math/Cargo.toml index 3aeb8f37f0..6455301cd0 100644 --- a/node-graph/nodes/math/Cargo.toml +++ b/node-graph/nodes/math/Cargo.toml @@ -9,6 +9,7 @@ license = "MIT OR Apache-2.0" [dependencies] core-types = { workspace = true } node-macro = { workspace = true } +graphic-types = { workspace = true } vector-types = { workspace = true } # Workspace dependencies diff --git a/node-graph/nodes/math/src/lib.rs b/node-graph/nodes/math/src/lib.rs index a5d92ec1ac..295e46d083 100644 --- a/node-graph/nodes/math/src/lib.rs +++ b/node-graph/nodes/math/src/lib.rs @@ -1,8 +1,11 @@ -use core_types::registry::types::{Fraction, Percentage, PixelSize, TextArea}; +use core_types::Context; +use core_types::registry::types::{Fraction, Percentage, PixelSize}; use core_types::table::Table; use core_types::transform::Footprint; use core_types::{Color, Ctx, num_traits}; use glam::{DAffine2, DVec2}; +use graphic_types::raster_types::{CPU, GPU, Raster}; +use graphic_types::{Artboard, Graphic, Vector}; use log::warn; use math_parser::ast; use math_parser::context::{EvalContext, NothingMap, ValueProvider}; @@ -735,6 +738,53 @@ fn logical_not( !input } +/// 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"))] +async fn switch( + #[implementations(Context)] ctx: C, + condition: bool, + #[expose] + #[implementations( + Context -> String, + Context -> bool, + Context -> f32, + Context -> f64, + Context -> u32, + Context -> u64, + Context -> DVec2, + Context -> DAffine2, + Context -> Table, + Context -> Table, + Context -> Table, + Context -> Table>, + Context -> Table>, + Context -> Table, + Context -> Table, + )] + if_true: impl Node, + #[expose] + #[implementations( + Context -> String, + Context -> bool, + Context -> f32, + Context -> f64, + Context -> u32, + Context -> u64, + Context -> DVec2, + Context -> DAffine2, + Context -> Table, + Context -> Table, + Context -> Table, + Context -> Table>, + Context -> Table>, + Context -> Table, + Context -> Table, + )] + if_false: impl Node, +) -> T { + if condition { if_true.eval(ctx).await } else { if_false.eval(ctx).await } +} + /// Constructs a bool value which may be set to true or false. #[node_macro::node(category("Value"))] fn bool_value(_: impl Ctx, _primary: (), #[name("Bool")] bool_value: bool) -> bool { @@ -823,12 +873,6 @@ fn sample_gradient(_: impl Ctx, _primary: (), gradient: Table, po Table::new_from_element(color) } -/// Constructs a string value which may be set to any plain text. -#[node_macro::node(category("Value"))] -fn string_value(_: impl Ctx, _primary: (), string: TextArea) -> String { - string -} - /// Constructs a footprint value which may be set to any transformation of a unit square describing a render area, and a render resolution at least 1x1 integer pixels. #[node_macro::node(category("Value"))] fn footprint_value(_: impl Ctx, _primary: (), transform: DAffine2, #[default(100., 100.)] resolution: PixelSize) -> Footprint { diff --git a/node-graph/nodes/text/Cargo.toml b/node-graph/nodes/text/Cargo.toml index 4537425a80..fa87f241ba 100644 --- a/node-graph/nodes/text/Cargo.toml +++ b/node-graph/nodes/text/Cargo.toml @@ -13,6 +13,7 @@ wasm = ["core-types/wasm", "tsify", "wasm-bindgen"] [dependencies] # Local dependencies core-types = { workspace = true } +raster-types = { workspace = true } vector-types = { workspace = true } node-macro = { workspace = true } @@ -22,6 +23,7 @@ glam = { workspace = true } parley = { workspace = true } skrifa = { workspace = true } log = { workspace = true } +serde_json = { workspace = true } # Optional workspace dependencies serde = { workspace = true, optional = true } diff --git a/node-graph/nodes/text/src/lib.rs b/node-graph/nodes/text/src/lib.rs index ca4738ffa1..d631a527db 100644 --- a/node-graph/nodes/text/src/lib.rs +++ b/node-graph/nodes/text/src/lib.rs @@ -3,13 +3,19 @@ mod path_builder; mod text_context; mod to_path; +use core_types::Color; +use core_types::Ctx; +use core_types::registry::types::TextArea; +use core_types::table::Table; use dyn_any::DynAny; -pub use font_cache::*; -pub use text_context::TextContext; -pub use to_path::*; +use glam::{DAffine2, DVec2}; +use raster_types::{CPU, Raster}; // Re-export for convenience pub use core_types as gcore; +pub use font_cache::*; +pub use text_context::TextContext; +pub use to_path::*; pub use vector_types; /// Alignment of lines of type within a text block. @@ -62,3 +68,134 @@ impl Default for TypesettingConfig { } } } + +/// Constructs a string value which may be set to any plain text. +#[node_macro::node(category("Value"))] +fn string_value(_: impl Ctx, _primary: (), string: TextArea) -> String { + string +} + +/// Type-asserts a value to be a string. +#[node_macro::node(category("Debug"))] +fn to_string(_: impl Ctx, value: String) -> String { + value +} + +/// Joins two strings together. +#[node_macro::node(category("Text"))] +fn string_concatenate(_: impl Ctx, #[implementations(String)] first: String, second: TextArea) -> String { + first.clone() + &second +} + +/// Replaces all occurrences of "From" with "To" in the input string. +#[node_macro::node(category("Text"))] +fn string_replace(_: impl Ctx, string: String, from: TextArea, to: TextArea) -> String { + string.replace(&from, &to) +} + +/// Extracts a substring from the input string, starting at "Start" and ending before "End". +/// Negative indices count from the end of the string. +/// If "Start" equals or exceeds "End", the result is an empty string. +#[node_macro::node(category("Text"))] +fn string_slice(_: impl Ctx, string: String, start: f64, end: f64) -> String { + let total_chars = string.chars().count(); + + let start = if start < 0. { + total_chars.saturating_sub(start.abs() as usize) + } else { + (start as usize).min(total_chars) + }; + let end = if end <= 0. { + total_chars.saturating_sub(end.abs() as usize) + } else { + (end as usize).min(total_chars) + }; + + if start >= end { + return String::new(); + } + + string.chars().skip(start).take(end - start).collect() +} + +// 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.) +/// Counts the number of characters in a string. +#[node_macro::node(category("Text"))] +fn string_length(_: impl Ctx, string: String) -> f64 { + string.chars().count() as f64 +} + +/// Splits a string into a list of substrings based on the specified delimeter. +/// For example, the delimeter "," will split "a,b,c" into the strings "a", "b", and "c". +#[node_macro::node(category("Text"))] +fn string_split( + _: impl Ctx, + /// The string to split into substrings. + string: String, + /// The character(s) that separate the substrings. These are not included in the outputs. + #[default("\\n")] + delimeter: String, + /// Whether to convert escape sequences found in the delimeter into their corresponding characters: + /// "\n" (newline), "\r" (carriage return), "\t" (tab), "\0" (null), and "\\" (backslash). + #[default(true)] + delimeter_escaping: bool, +) -> Vec { + 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() +} + +/// Gets a value from either a json object or array given as a string input. +/// For example, for the input {"name": "ferris"} the key "name" will return "ferris". +#[node_macro::node(category("Text"))] +fn json_get( + _: impl Ctx, + /// The json data. + data: String, + /// The key to index the object with. + key: String, +) -> String { + use serde_json::Value; + let Ok(value): Result = serde_json::from_str(&data) else { + return "Input is not valid json".into(); + }; + match value { + Value::Array(ref arr) => { + let Ok(index): Result = key.parse() else { + log::error!("Json input is an array, but key is not a number"); + return String::new(); + }; + let Some(value) = arr.get(index) else { + log::error!("Index {} out of bounds for len {}", index, arr.len()); + return String::new(); + }; + value.to_string() + } + Value::Object(map) => { + let Some(value) = map.get(&key) else { + log::error!("Key {key} not found in object"); + return String::new(); + }; + match value { + Value::String(s) => s.clone(), + Value::Number(n) => n.to_string(), + complex => complex.to_string(), + } + } + _ => String::new(), + } +} + +/// Converts a value to a JSON string representation. +#[node_macro::node(category("Text"))] +fn serialize( + _: impl Ctx, + #[implementations(String, bool, f64, u32, u64, DVec2, DAffine2, /* Table, Table, Table, */ Table>, Table /* , Table */)] value: T, +) -> String { + serde_json::to_string(&value).unwrap_or_else(|_| "Serialization Error".to_string()) +}