mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-17 23:38:06 +08:00
Add the auto-generated node catalog to the website's user manual (#3662)
* Generate the MVP node catalog in the manual (with some placeholders) * Implement nearly the rest of everything * Move to the tools directory and make it generate nicer default values * Add category descriptions * Organize file structure and improve type naming * Improve book table of contents code * Add collapsing chapter navigation to the book template * Add to build workflow * Clean up site structure
This commit is contained in:
@@ -116,15 +116,15 @@ macro_rules! tagged_value {
|
||||
_ => Err(format!("Cannot convert {:?} to TaggedValue",std::any::type_name_of_val(input))),
|
||||
}
|
||||
}
|
||||
/// Returns a TaggedValue from the type, where that value is its type's `Default::default()`
|
||||
pub fn from_type(input: &Type) -> Option<Self> {
|
||||
match input {
|
||||
Type::Generic(_) => None,
|
||||
Type::Concrete(concrete_type) => {
|
||||
let internal_id = concrete_type.id?;
|
||||
use std::any::TypeId;
|
||||
// TODO: Add default implementations for types such as TaggedValue::Subpaths, and use the defaults here and in document_node_types
|
||||
// Tries using the default for the tagged value type. If it not implemented, then uses the default used in document_node_types. If it is not used there, then TaggedValue::None is returned.
|
||||
Some(match internal_id {
|
||||
Some(match concrete_type.id? {
|
||||
x if x == TypeId::of::<()>() => TaggedValue::None,
|
||||
$( x if x == TypeId::of::<$ty>() => TaggedValue::$identifier(Default::default()), )*
|
||||
_ => return None,
|
||||
@@ -139,6 +139,15 @@ macro_rules! tagged_value {
|
||||
pub fn from_type_or_none(input: &Type) -> Self {
|
||||
Self::from_type(input).unwrap_or(TaggedValue::None)
|
||||
}
|
||||
pub fn to_debug_string(&self) -> String {
|
||||
match self {
|
||||
Self::None => "()".to_string(),
|
||||
$( Self::$identifier(x) => format!("{:?}", x), )*
|
||||
Self::RenderOutput(_) => "RenderOutput".to_string(),
|
||||
Self::SurfaceFrame(_) => "SurfaceFrame".to_string(),
|
||||
Self::EditorApi(_) => "WasmEditorApi".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$(
|
||||
@@ -351,24 +360,24 @@ impl TaggedValue {
|
||||
match ty {
|
||||
Type::Generic(_) => None,
|
||||
Type::Concrete(concrete_type) => {
|
||||
let internal_id = concrete_type.id?;
|
||||
let ty = concrete_type.id?;
|
||||
use std::any::TypeId;
|
||||
// TODO: Add default implementations for types such as TaggedValue::Subpaths, and use the defaults here and in document_node_types
|
||||
// Tries using the default for the tagged value type. If it not implemented, then uses the default used in document_node_types. If it is not used there, then TaggedValue::None is returned.
|
||||
let ty = match internal_id {
|
||||
x if x == TypeId::of::<()>() => TaggedValue::None,
|
||||
x if x == TypeId::of::<String>() => TaggedValue::String(string.into()),
|
||||
x if x == TypeId::of::<f64>() => FromStr::from_str(string).map(TaggedValue::F64).ok()?,
|
||||
x if x == TypeId::of::<f32>() => FromStr::from_str(string).map(TaggedValue::F32).ok()?,
|
||||
x if x == TypeId::of::<u64>() => FromStr::from_str(string).map(TaggedValue::U64).ok()?,
|
||||
x if x == TypeId::of::<u32>() => FromStr::from_str(string).map(TaggedValue::U32).ok()?,
|
||||
x if x == TypeId::of::<DVec2>() => to_dvec2(string).map(TaggedValue::DVec2)?,
|
||||
x if x == TypeId::of::<bool>() => FromStr::from_str(string).map(TaggedValue::Bool).ok()?,
|
||||
x if x == TypeId::of::<Color>() => to_color(string).map(TaggedValue::ColorNotInTable)?,
|
||||
x if x == TypeId::of::<Option<Color>>() => TaggedValue::ColorNotInTable(to_color(string)?),
|
||||
x if x == TypeId::of::<Table<Color>>() => to_color(string).map(|color| TaggedValue::Color(Table::new_from_element(color)))?,
|
||||
x if x == TypeId::of::<Fill>() => to_color(string).map(|color| TaggedValue::Fill(Fill::solid(color)))?,
|
||||
x if x == TypeId::of::<ReferencePoint>() => to_reference_point(string).map(TaggedValue::ReferencePoint)?,
|
||||
let ty = match () {
|
||||
() if ty == TypeId::of::<()>() => TaggedValue::None,
|
||||
() if ty == TypeId::of::<String>() => TaggedValue::String(string.into()),
|
||||
() if ty == TypeId::of::<f64>() => FromStr::from_str(string).map(TaggedValue::F64).ok()?,
|
||||
() if ty == TypeId::of::<f32>() => FromStr::from_str(string).map(TaggedValue::F32).ok()?,
|
||||
() if ty == TypeId::of::<u64>() => FromStr::from_str(string).map(TaggedValue::U64).ok()?,
|
||||
() if ty == TypeId::of::<u32>() => FromStr::from_str(string).map(TaggedValue::U32).ok()?,
|
||||
() if ty == TypeId::of::<DVec2>() => to_dvec2(string).map(TaggedValue::DVec2)?,
|
||||
() if ty == TypeId::of::<bool>() => FromStr::from_str(string).map(TaggedValue::Bool).ok()?,
|
||||
() if ty == TypeId::of::<Color>() => to_color(string).map(TaggedValue::ColorNotInTable)?,
|
||||
() if ty == TypeId::of::<Option<Color>>() => TaggedValue::ColorNotInTable(to_color(string)?),
|
||||
() if ty == TypeId::of::<Table<Color>>() => to_color(string).map(|color| TaggedValue::Color(Table::new_from_element(color)))?,
|
||||
() if ty == TypeId::of::<Fill>() => to_color(string).map(|color| TaggedValue::Fill(Fill::solid(color)))?,
|
||||
() if ty == TypeId::of::<ReferencePoint>() => to_reference_point(string).map(TaggedValue::ReferencePoint)?,
|
||||
_ => return None,
|
||||
};
|
||||
Some(ty)
|
||||
|
||||
@@ -21,7 +21,7 @@ pub fn detect_file_type(path: &Path) -> Result<FileType, String> {
|
||||
Some("svg") => Ok(FileType::Svg),
|
||||
Some("png") => Ok(FileType::Png),
|
||||
Some("jpg" | "jpeg") => Ok(FileType::Jpg),
|
||||
_ => Err(format!("Unsupported file extension. Supported formats: .svg, .png, .jpg")),
|
||||
_ => Err("Unsupported file extension. Supported formats: .svg, .png, .jpg".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,8 +31,7 @@ pub async fn export_document(
|
||||
output_path: PathBuf,
|
||||
file_type: FileType,
|
||||
scale: f64,
|
||||
width: Option<u32>,
|
||||
height: Option<u32>,
|
||||
(width, height): (Option<u32>, Option<u32>),
|
||||
transparent: bool,
|
||||
) -> Result<(), Box<dyn Error>> {
|
||||
// Determine export format based on file type
|
||||
@@ -42,10 +41,12 @@ pub async fn export_document(
|
||||
};
|
||||
|
||||
// Create render config with export settings
|
||||
let mut render_config = RenderConfig::default();
|
||||
render_config.export_format = export_format;
|
||||
render_config.for_export = true;
|
||||
render_config.scale = scale;
|
||||
let mut render_config = RenderConfig {
|
||||
scale,
|
||||
export_format,
|
||||
for_export: true,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
// Set viewport dimensions if specified
|
||||
if let (Some(w), Some(h)) = (width, height) {
|
||||
|
||||
@@ -97,9 +97,9 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
Command::Compile { ref document, .. } => document,
|
||||
Command::Export { ref document, .. } => document,
|
||||
Command::ListNodeIdentifiers => {
|
||||
let mut ids: Vec<_> = graphene_std::registry::NODE_METADATA.lock().unwrap().keys().cloned().collect();
|
||||
ids.sort_by_key(|x| x.as_str().to_string());
|
||||
for id in ids {
|
||||
let mut nodes: Vec<_> = graphene_std::registry::NODE_METADATA.lock().unwrap().keys().cloned().collect();
|
||||
nodes.sort_by_key(|x| x.as_str().to_string());
|
||||
for id in nodes {
|
||||
println!("{}", id.as_str());
|
||||
}
|
||||
return Ok(());
|
||||
@@ -108,7 +108,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
|
||||
let document_string = std::fs::read_to_string(document_path).expect("Failed to read document");
|
||||
|
||||
log::info!("creating gpu context",);
|
||||
log::info!("Creating GPU context");
|
||||
let mut application_io = block_on(WasmApplicationIo::new_offscreen());
|
||||
|
||||
if let Command::Export { image: Some(ref image_path), .. } = app.command {
|
||||
@@ -164,7 +164,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
let executor = create_executor(proto_graph)?;
|
||||
|
||||
// Perform export
|
||||
export::export_document(&executor, wgpu_executor_ref, output, file_type, scale, width, height, transparent).await?;
|
||||
export::export_document(&executor, wgpu_executor_ref, output, file_type, scale, (width, height), transparent).await?;
|
||||
}
|
||||
_ => unreachable!("All other commands should be handled before this match statement is run"),
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ use graphene_std::wasm_application_io::WasmEditorApi;
|
||||
use graphene_std::wasm_application_io::WasmSurfaceHandle;
|
||||
use graphene_std::{Artboard, Context, Graphic, NodeIO, NodeIOTypes, ProtoNodeIdentifier, concrete, fn_type_fut, future};
|
||||
use node_registry_macros::{async_node, convert_node, into_node};
|
||||
use once_cell::sync::Lazy;
|
||||
use std::collections::HashMap;
|
||||
#[cfg(feature = "gpu")]
|
||||
use std::sync::Arc;
|
||||
@@ -282,7 +281,8 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
|
||||
map
|
||||
}
|
||||
|
||||
pub static NODE_REGISTRY: Lazy<HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeConstructor>>> = Lazy::new(|| node_registry());
|
||||
// TODO: Replace with `core::cell::LazyCell` (<https://doc.rust-lang.org/core/cell/struct.LazyCell.html>) or similar
|
||||
pub static NODE_REGISTRY: once_cell::sync::Lazy<HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeConstructor>>> = once_cell::sync::Lazy::new(|| node_registry());
|
||||
|
||||
mod node_registry_macros {
|
||||
macro_rules! async_node {
|
||||
|
||||
@@ -113,6 +113,20 @@ bitflags! {
|
||||
}
|
||||
}
|
||||
|
||||
impl ContextFeatures {
|
||||
pub fn name(&self) -> &'static str {
|
||||
match *self {
|
||||
ContextFeatures::FOOTPRINT => "Footprint",
|
||||
ContextFeatures::REAL_TIME => "RealTime",
|
||||
ContextFeatures::ANIMATION_TIME => "AnimationTime",
|
||||
ContextFeatures::POINTER => "Pointer",
|
||||
ContextFeatures::INDEX => "Index",
|
||||
ContextFeatures::VARARGS => "VarArgs",
|
||||
_ => "Multiple Features",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, dyn_any::DynAny, serde::Serialize, serde::Deserialize, Default)]
|
||||
pub struct ContextDependencies {
|
||||
pub extract: ContextFeatures,
|
||||
|
||||
@@ -11,7 +11,7 @@ use std::sync::{LazyLock, Mutex};
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct NodeMetadata {
|
||||
pub display_name: &'static str,
|
||||
pub category: Option<&'static str>,
|
||||
pub category: &'static str,
|
||||
pub fields: Vec<FieldMetadata>,
|
||||
pub description: &'static str,
|
||||
pub properties: Option<&'static str>,
|
||||
@@ -23,6 +23,7 @@ pub struct NodeMetadata {
|
||||
pub struct FieldMetadata {
|
||||
pub name: &'static str,
|
||||
pub description: &'static str,
|
||||
pub hidden: bool,
|
||||
pub exposed: bool,
|
||||
pub widget_override: RegistryWidgetOverride,
|
||||
pub value_source: RegistryValueSource,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::transform::Footprint;
|
||||
use std::any::TypeId;
|
||||
pub use std::borrow::Cow;
|
||||
use std::fmt::{Display, Formatter};
|
||||
@@ -75,6 +76,7 @@ macro_rules! fn_type_fut {
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: Rename to NodeSignatureMonomorphization
|
||||
#[derive(Clone, PartialEq, Eq, Hash, Default, serde::Serialize, serde::Deserialize)]
|
||||
pub struct NodeIOTypes {
|
||||
pub call_argument: Type,
|
||||
@@ -351,7 +353,10 @@ pub fn format_type(ty: &str) -> String {
|
||||
}
|
||||
|
||||
pub fn make_type_user_readable(ty: &str) -> String {
|
||||
ty.replace("Option<Arc<OwnedContextImpl>>", "Context").replace("Vector<Option<Table<Graphic>>>", "Vector")
|
||||
ty.replace("Option<Arc<OwnedContextImpl>>", "Context")
|
||||
.replace("Vector<Option<Table<Graphic>>>", "Vector")
|
||||
.replace("Raster<CPU>", "Raster")
|
||||
.replace("Raster<GPU>", "Raster")
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Type {
|
||||
@@ -372,6 +377,19 @@ impl std::fmt::Debug for Type {
|
||||
|
||||
impl std::fmt::Display for Type {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
if self == &concrete!(glam::DVec2) {
|
||||
return write!(f, "vec2");
|
||||
}
|
||||
if self == &concrete!(glam::DAffine2) {
|
||||
return write!(f, "transform");
|
||||
}
|
||||
if self == &concrete!(Footprint) {
|
||||
return write!(f, "footprint");
|
||||
}
|
||||
if self == &concrete!(&str) || self == &concrete!(String) {
|
||||
return write!(f, "string");
|
||||
}
|
||||
|
||||
let text = match self {
|
||||
Type::Generic(name) => name.to_string(),
|
||||
Type::Concrete(ty) => format_type(&ty.name),
|
||||
|
||||
@@ -23,8 +23,8 @@ proc-macro2 = { workspace = true }
|
||||
quote = { workspace = true }
|
||||
convert_case = { workspace = true }
|
||||
strum = { workspace = true }
|
||||
indoc = { workspace = true }
|
||||
|
||||
indoc = "2.0.5"
|
||||
proc-macro-crate = "3.1.0"
|
||||
proc-macro-error2 = "2"
|
||||
|
||||
|
||||
@@ -28,7 +28,10 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
|
||||
} = parsed;
|
||||
let core_types = crate_ident.gcore()?;
|
||||
|
||||
let category = &attributes.category.as_ref().map(|value| quote!(Some(#value))).unwrap_or(quote!(None));
|
||||
let category = attributes
|
||||
.category
|
||||
.as_ref()
|
||||
.expect("The 'category' attribute is required and should be checked during parsing, but was not found during codegen");
|
||||
let mod_name = format_ident!("_{}_mod", mod_name);
|
||||
|
||||
let display_name = match &attributes.display_name.as_ref() {
|
||||
@@ -98,6 +101,8 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
|
||||
})
|
||||
.collect();
|
||||
|
||||
let input_hidden = regular_field_names.iter().map(|name| name.to_string().starts_with('_')).collect::<Vec<_>>();
|
||||
|
||||
let input_descriptions: Vec<_> = regular_fields.iter().map(|f| &f.description).collect();
|
||||
|
||||
// Generate struct fields: data fields (concrete types) + regular fields (generic types)
|
||||
@@ -475,6 +480,7 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
|
||||
name: #input_names,
|
||||
widget_override: #widget_override,
|
||||
description: #input_descriptions,
|
||||
hidden: #input_hidden,
|
||||
exposed: #exposed,
|
||||
value_source: #value_sources,
|
||||
default_type: #default_types,
|
||||
|
||||
@@ -211,9 +211,13 @@ impl Parse for NodeFnAttributes {
|
||||
// syn::parenthesized!(content in input);
|
||||
|
||||
let nested = content.call(Punctuated::<Meta, Comma>::parse_terminated)?;
|
||||
for meta in nested {
|
||||
for meta in nested.iter() {
|
||||
let name = meta.path().get_ident().ok_or_else(|| Error::new_spanned(meta.path(), "Node macro expects a known Ident, not a path"))?;
|
||||
match name.to_string().as_str() {
|
||||
// User-facing category in the node catalog. The empty string `category("")` hides the node from the catalog.
|
||||
//
|
||||
// Example usage:
|
||||
// #[node_macro::node(..., category("Math: Arithmetic"), ...)]
|
||||
"category" => {
|
||||
let meta = meta.require_list()?;
|
||||
if category.is_some() {
|
||||
@@ -224,6 +228,11 @@ impl Parse for NodeFnAttributes {
|
||||
.map_err(|_| Error::new_spanned(meta, "Expected a string literal for 'category', e.g., category(\"Value\")"))?;
|
||||
category = Some(lit);
|
||||
}
|
||||
// Override for the display name in the node catalog in place of the auto-generated name taken from the function name with inferred Title Case formatting.
|
||||
// Use this if capitalization or formatting needs to be overridden.
|
||||
//
|
||||
// Example usage:
|
||||
// #[node_macro::node(..., name("Request URL"), ...)]
|
||||
"name" => {
|
||||
let meta = meta.require_list()?;
|
||||
if display_name.is_some() {
|
||||
@@ -232,6 +241,12 @@ impl Parse for NodeFnAttributes {
|
||||
let parsed_name: LitStr = meta.parse_args().map_err(|_| Error::new_spanned(meta, "Expected a string for 'name', e.g., name(\"Memoize\")"))?;
|
||||
display_name = Some(parsed_name);
|
||||
}
|
||||
// Override for the fully qualified path used by Graphene to identify the node implementation.
|
||||
// If not provided, the path will be inferred from the module path and function name.
|
||||
// Use this if the node implementation has moved to a different module or crate but a migration to that new path is not desired.
|
||||
//
|
||||
// Example usage:
|
||||
// #[node_macro::node(..., path(core_types::vector), ...)]
|
||||
"path" => {
|
||||
let meta = meta.require_list()?;
|
||||
if path.is_some() {
|
||||
@@ -242,6 +257,13 @@ impl Parse for NodeFnAttributes {
|
||||
.map_err(|_| Error::new_spanned(meta, "Expected a valid path for 'path', e.g., path(crate::MemoizeNode)"))?;
|
||||
path = Some(parsed_path);
|
||||
}
|
||||
// Indicator that the node should allow generic type arguments but skip the automatic generation of concrete type implementations.
|
||||
// It allows the type arguments in this node to not include the normally required `#[implementations(...)]` attribute on each generic parameter.
|
||||
// Instead, concrete implementations must be manually listed in the Node Registry, or where impossible, produced at runtime by the compile server.
|
||||
// This is used by a few advanced nodes that need to support many types where listing them all would be cumbersome or impossible.
|
||||
//
|
||||
// Example usage:
|
||||
// #[node_macro::node(..., skip_impl, ...)]
|
||||
"skip_impl" => {
|
||||
let path = meta.require_path_only()?;
|
||||
if skip_impl {
|
||||
@@ -249,31 +271,48 @@ impl Parse for NodeFnAttributes {
|
||||
}
|
||||
skip_impl = true;
|
||||
}
|
||||
// Override UI layout generator function name defined in `node_properties.rs` that returns a custom Properties panel layout for this node.
|
||||
// This is used to create custom UI for the input parameters of the node in cases where the defaults generated from the type and attributes are insufficient.
|
||||
//
|
||||
// Example usage:
|
||||
// #[node_macro::node(..., properties("channel_mixer_properties"), ...)]
|
||||
"properties" => {
|
||||
let meta = meta.require_list()?;
|
||||
if properties_string.is_some() {
|
||||
return Err(Error::new_spanned(path, "Multiple 'properties_string' attributes are not allowed"));
|
||||
return Err(Error::new_spanned(path, "Multiple 'properties' attributes are not allowed"));
|
||||
}
|
||||
let parsed_properties_string: LitStr = meta
|
||||
.parse_args()
|
||||
.map_err(|_| Error::new_spanned(meta, "Expected a string for 'properties', e.g., name(\"channel_mixer_properties\")"))?;
|
||||
.map_err(|_| Error::new_spanned(meta, "Expected a string for 'properties', e.g., properties(\"channel_mixer_properties\")"))?;
|
||||
|
||||
properties_string = Some(parsed_properties_string);
|
||||
}
|
||||
// Conditional compilation tokens to gate when this node is included in the build.
|
||||
//
|
||||
// Example usage:
|
||||
// #[node_macro::node(..., cfg(feature = "std"), ...)]
|
||||
"cfg" => {
|
||||
if cfg.is_some() {
|
||||
return Err(Error::new_spanned(path, "Multiple 'feature' attributes are not allowed"));
|
||||
return Err(Error::new_spanned(path, "Multiple 'cfg' attributes are not allowed"));
|
||||
}
|
||||
let meta = meta.require_list()?;
|
||||
cfg = Some(meta.tokens.clone());
|
||||
}
|
||||
// Reference to a specific shader definition struct that is used to run the logic of this node on the GPU.
|
||||
//
|
||||
// Example usage:
|
||||
// #[node_macro::node(..., shader_node(PerPixelAdjust), ...)]
|
||||
"shader_node" => {
|
||||
if shader_node.is_some() {
|
||||
return Err(Error::new_spanned(path, "Multiple 'feature' attributes are not allowed"));
|
||||
return Err(Error::new_spanned(path, "Multiple 'shader_node' attributes are not allowed"));
|
||||
}
|
||||
let meta = meta.require_list()?;
|
||||
shader_node = Some(syn::parse2(meta.tokens.to_token_stream())?);
|
||||
}
|
||||
// Function name for custom serialization of this node's data. This is only used by the Monitor node.
|
||||
//
|
||||
// Example usage:
|
||||
// #[node_macro::node(..., serialize(my_module::custom_serialize), ...)]
|
||||
"serialize" => {
|
||||
let meta = meta.require_list()?;
|
||||
if serialize.is_some() {
|
||||
@@ -290,10 +329,9 @@ impl Parse for NodeFnAttributes {
|
||||
indoc!(
|
||||
r#"
|
||||
Unsupported attribute in `node`.
|
||||
Supported attributes are 'category', 'path', 'name', 'skip_impl', 'cfg', 'properties', 'serialize', and 'shader_node'.
|
||||
|
||||
Supported attributes are 'category', 'name', 'path', 'skip_impl', 'properties', 'cfg', 'shader_node', and 'serialize'.
|
||||
Example usage:
|
||||
#[node_macro::node(category("Value"), name("Test Node"))]
|
||||
#[node_macro::node(..., name("Test Node"), ...)]
|
||||
"#
|
||||
),
|
||||
));
|
||||
@@ -301,6 +339,19 @@ impl Parse for NodeFnAttributes {
|
||||
}
|
||||
}
|
||||
|
||||
if category.is_none() {
|
||||
return Err(Error::new_spanned(
|
||||
nested,
|
||||
indoc!(
|
||||
r#"
|
||||
The attribute 'category' is required.
|
||||
Example usage:
|
||||
#[node_macro::node(..., category("Value"), ...)]
|
||||
"#,
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(NodeFnAttributes {
|
||||
category,
|
||||
display_name,
|
||||
@@ -315,7 +366,7 @@ impl Parse for NodeFnAttributes {
|
||||
}
|
||||
|
||||
fn parse_node_fn(attr: TokenStream2, item: TokenStream2) -> syn::Result<ParsedNodeFn> {
|
||||
let attributes = syn::parse2::<NodeFnAttributes>(attr.clone()).map_err(|e| Error::new(e.span(), format!("Failed to parse node_fn attributes: {e}")))?;
|
||||
let attributes = syn::parse2::<NodeFnAttributes>(attr.clone()).map_err(|e| Error::new(e.span(), format!("Failed to parse node_fn attributes:\n{e}")))?;
|
||||
let input_fn = syn::parse2::<ItemFn>(item.clone()).map_err(|e| Error::new(e.span(), format!("Failed to parse function: {e}. Make sure it's a valid Rust function.")))?;
|
||||
|
||||
let vis = input_fn.vis;
|
||||
@@ -482,7 +533,16 @@ fn parse_node_implementations<T: Parse>(attr: &Attribute, name: &Ident) -> syn::
|
||||
fn parse_field(pat_ident: PatIdent, ty: Type, attrs: &[Attribute]) -> syn::Result<ParsedField> {
|
||||
let ident = &pat_ident.ident;
|
||||
|
||||
// Check if this is a data field (struct field, not a parameter)
|
||||
// Checks for the #[data] attribute, indicating that this is a data field rather than an input parameter to the node.
|
||||
// Data fields act as internal state, using interior mutability to cache data between node evaluations.
|
||||
//
|
||||
// Normally, an input parameter is a construction argument to the node that is stored as a field on the node struct.
|
||||
// Specifically, its struct field stores the connected upstream node (an evaluatable lambda that returns data of the connection wire's type).
|
||||
// By comparison, a data field is also stored as a field on the node struct, allowing it to persist state between evaluations.
|
||||
// But it acts as internal state only, not exposed as a parameter in the UI or able to be wired to another node.
|
||||
//
|
||||
// Nodes implemented using a data field must ensure the persistent state is used in a manner that respects the invariant of idempotence,
|
||||
// meaning the node's output is always deterministic whether or not the internal state is present.
|
||||
let is_data_field = extract_attribute(attrs, "data").is_some();
|
||||
|
||||
let default_value = extract_attribute(attrs, "default")
|
||||
@@ -723,10 +783,10 @@ fn extract_attribute<'a>(attrs: &'a [Attribute], name: &str) -> Option<&'a Attri
|
||||
// Modify the new_node_fn function to use the code generation
|
||||
pub fn new_node_fn(attr: TokenStream2, item: TokenStream2) -> syn::Result<TokenStream2> {
|
||||
let crate_ident = CrateIdent::default();
|
||||
let mut parsed_node = parse_node_fn(attr, item.clone()).map_err(|e| Error::new(e.span(), format!("Failed to parse node function: {e}")))?;
|
||||
let mut parsed_node = parse_node_fn(attr, item.clone()).map_err(|e| Error::new(e.span(), format!("Failed to parse node function:\n{e}")))?;
|
||||
parsed_node.replace_impl_trait_in_input();
|
||||
crate::validation::validate_node_fn(&parsed_node).map_err(|e| Error::new(e.span(), format!("Validation Error: {e}")))?;
|
||||
generate_node_code(&crate_ident, &parsed_node).map_err(|e| Error::new(e.span(), format!("Failed to generate node code: {e}")))
|
||||
crate::validation::validate_node_fn(&parsed_node).map_err(|e| Error::new(e.span(), format!("Validation error:\n{e}")))?;
|
||||
generate_node_code(&crate_ident, &parsed_node).map_err(|e| Error::new(e.span(), format!("Failed to generate node code:\n{e}")))
|
||||
}
|
||||
|
||||
impl ParsedNodeFn {
|
||||
|
||||
@@ -117,7 +117,7 @@ fn validate_implementations_for_generics(parsed: &ParsedNodeFn) {
|
||||
quote!(#ty),
|
||||
pat_ident.ident;
|
||||
help = "Add #[implementations(ConcreteType1, ConcreteType2)] to field '{}'", pat_ident.ident;
|
||||
help = "Or use #[node_macro::node(skip_impl)] if you want to manually implement the node"
|
||||
help = "Or use #[node_macro::node(category(...), skip_impl)] if you want to manually implement the node"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -133,7 +133,7 @@ fn validate_implementations_for_generics(parsed: &ParsedNodeFn) {
|
||||
"Generic types in Node field `{}` require an #[implementations(...)] attribute",
|
||||
pat_ident.ident;
|
||||
help = "Add #[implementations(InputType1 -> OutputType1, InputType2 -> OutputType2)] to field '{}'", pat_ident.ident;
|
||||
help = "Or use #[node_macro::node(skip_impl)] if you want to manually implement the node"
|
||||
help = "Or use #[node_macro::node(category(...), skip_impl)] if you want to manually implement the node"
|
||||
);
|
||||
}
|
||||
// Additional check for Node implementations
|
||||
|
||||
@@ -176,7 +176,7 @@ impl SetClip for Table<GradientStops> {
|
||||
}
|
||||
|
||||
/// Applies the blend mode to the input graphics. Setting this allows for customizing how overlapping content is composited together.
|
||||
#[node_macro::node(category("Style"))]
|
||||
#[node_macro::node(category("Blending"))]
|
||||
fn blend_mode<T: SetBlendMode>(
|
||||
_: impl Ctx,
|
||||
/// The layer stack that will be composited when rendering.
|
||||
@@ -198,7 +198,7 @@ fn blend_mode<T: SetBlendMode>(
|
||||
|
||||
/// Modifies the opacity of the input graphics by multiplying the existing opacity by this percentage.
|
||||
/// This affects the transparency of the content (together with anything above which is clipped to it).
|
||||
#[node_macro::node(category("Style"))]
|
||||
#[node_macro::node(category("Blending"))]
|
||||
fn opacity<T: MultiplyAlpha>(
|
||||
_: impl Ctx,
|
||||
/// The layer stack that will be composited when rendering.
|
||||
@@ -221,7 +221,7 @@ fn opacity<T: MultiplyAlpha>(
|
||||
}
|
||||
|
||||
/// Sets each of the blending properties at once. The blend mode determines how overlapping content is composited together. The opacity affects the transparency of the content (together with anything above which is clipped to it). The fill affects the transparency of the content itself, without affecting that of content clipped to it. The clip property determines whether the content inherits the alpha of the content beneath it.
|
||||
#[node_macro::node(category("Style"))]
|
||||
#[node_macro::node(category("Blending"))]
|
||||
fn blending<T: SetBlendMode + MultiplyAlpha + MultiplyFill + SetClip>(
|
||||
_: impl Ctx,
|
||||
/// The layer stack that will be composited when rendering.
|
||||
|
||||
@@ -184,7 +184,7 @@ pub fn blend_with_mode(background: TableRow<Raster<CPU>>, foreground: TableRow<R
|
||||
|
||||
/// Generates the brush strokes painted with the Brush tool as a raster image.
|
||||
/// If an input image is supplied, strokes are drawn on top of it, expanding bounds as needed.
|
||||
#[node_macro::node(category("Raster"))]
|
||||
#[node_macro::node(category(""))]
|
||||
async fn brush(
|
||||
_: impl Ctx,
|
||||
/// Optional raster content that may be drawn onto.
|
||||
|
||||
@@ -856,7 +856,7 @@ fn angle_to<T: ToPosition, U: ToPosition>(
|
||||
#[expose]
|
||||
#[implementations(DVec2, DVec2, DAffine2, DAffine2)]
|
||||
target: U,
|
||||
/// Whether the resulting angle should be given in as radians instead of degrees.
|
||||
/// Whether the resulting angle should be given in radians instead of degrees.
|
||||
radians: bool,
|
||||
) -> f64 {
|
||||
let from = observer.to_position();
|
||||
|
||||
@@ -73,7 +73,7 @@ fn luminance<T: Adjust<Color>>(
|
||||
input
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Raster"), shader_node(PerPixelAdjust))]
|
||||
#[node_macro::node(category("Raster: Adjustment"), shader_node(PerPixelAdjust))]
|
||||
fn gamma_correction<T: Adjust<Color>>(
|
||||
_: impl Ctx,
|
||||
#[implementations(
|
||||
|
||||
@@ -1200,7 +1200,7 @@ async fn separate_subpaths(_: impl Ctx, content: Table<Vector>) -> Table<Vector>
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Vector: Modifier"), path(graphene_core::vector))]
|
||||
#[node_macro::node(category("Vector"), path(graphene_core::vector))]
|
||||
fn instance_vector(ctx: impl Ctx + ExtractVarArgs) -> Table<Vector> {
|
||||
let Ok(var_arg) = ctx.vararg(0) else { return Default::default() };
|
||||
let var_arg = var_arg as &dyn std::any::Any;
|
||||
@@ -1208,7 +1208,7 @@ fn instance_vector(ctx: impl Ctx + ExtractVarArgs) -> Table<Vector> {
|
||||
var_arg.downcast_ref().cloned().unwrap_or_default()
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Vector: Modifier"), path(graphene_core::vector))]
|
||||
#[node_macro::node(category("Vector"), path(graphene_core::vector))]
|
||||
async fn instance_map(ctx: impl Ctx + CloneVarArgs + ExtractAll, content: Table<Vector>, mapped: impl Node<Context<'static>, Output = Table<Vector>>) -> Table<Vector> {
|
||||
let mut rows = Vec::new();
|
||||
|
||||
@@ -2266,7 +2266,7 @@ async fn count_points(_: impl Ctx, content: Table<Vector>) -> f64 {
|
||||
|
||||
/// Retrieves the vec2 position (in local space) of the anchor point at the specified index in table of vector elements.
|
||||
/// If no value exists at that index, the position (0, 0) is returned.
|
||||
#[node_macro::node(category("Vector"), path(graphene_core::vector))]
|
||||
#[node_macro::node(category("Vector: Measure"), path(graphene_core::vector))]
|
||||
async fn index_points(
|
||||
_: impl Ctx,
|
||||
/// The vector element or elements containing the anchor points to be retrieved.
|
||||
|
||||
@@ -37,7 +37,7 @@ pub fn generate_node_substitutions() -> HashMap<ProtoNodeIdentifier, DocumentNod
|
||||
|
||||
let NodeMetadata { fields, .. } = metadata;
|
||||
let Some(implementations) = &node_registry.get(&id) else { continue };
|
||||
let valid_inputs: HashSet<_> = implementations.iter().map(|(_, node_io)| node_io.call_argument.clone()).collect();
|
||||
let valid_call_args: HashSet<_> = implementations.iter().map(|(_, node_io)| node_io.call_argument.clone()).collect();
|
||||
let first_node_io = implementations.first().map(|(_, node_io)| node_io).unwrap_or(const { &NodeIOTypes::empty() });
|
||||
let mut node_io_types = vec![HashSet::new(); fields.len()];
|
||||
for (_, node_io) in implementations.iter() {
|
||||
@@ -46,7 +46,7 @@ pub fn generate_node_substitutions() -> HashMap<ProtoNodeIdentifier, DocumentNod
|
||||
}
|
||||
}
|
||||
let mut input_type = &first_node_io.call_argument;
|
||||
if valid_inputs.len() > 1 {
|
||||
if valid_call_args.len() > 1 {
|
||||
input_type = &const { generic!(D) };
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user