Parse description from node doc comments (#2089)

* Parse description from node doc comments

* Add node description tooltips

* Code review

---------

Co-authored-by: Adam G <adamgerhant@gmail.com>
Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
Dennis Kobert 2024-11-03 23:57:20 +01:00 committed by GitHub
parent 8fdecaa487
commit 35f7cfac80
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 144 additions and 24 deletions

View file

@ -1,5 +1,6 @@
use crate::messages::debug::utility_types::MessageLoggingVerbosity;
use crate::messages::dialog::DialogMessageData;
use crate::messages::portfolio::document::node_graph::document_node_definitions;
use crate::messages::prelude::*;
use graphene_core::text::Font;
@ -137,6 +138,13 @@ impl Dispatcher {
// Load the default font
let font = Font::new(graphene_core::consts::DEFAULT_FONT_FAMILY.into(), graphene_core::consts::DEFAULT_FONT_STYLE.into());
queue.add(FrontendMessage::TriggerFontLoad { font, is_default: true });
// Send the information for tooltips and categories for each node/input.
queue.add(FrontendMessage::SendUIMetadata {
input_type_descriptions: Vec::new(),
node_descriptions: document_node_definitions::collect_node_descriptions(),
node_types: document_node_definitions::collect_node_types(),
});
}
Message::Batched(messages) => {
messages.iter().for_each(|message| self.handle_message(message.to_owned()));

View file

@ -39,6 +39,16 @@ pub enum FrontendMessage {
},
DisplayRemoveEditableTextbox,
// Send prefix: Send global, static data to the frontend that is never updated
SendUIMetadata {
#[serde(rename = "inputTypeDescriptions")]
input_type_descriptions: Vec<(String, String)>,
#[serde(rename = "nodeDescriptions")]
node_descriptions: Vec<(String, String)>,
#[serde(rename = "nodeTypes")]
node_types: Vec<FrontendNodeType>,
},
// Trigger prefix: cause a browser API to do something
TriggerAboutGraphiteLocalizedCommitDate {
#[serde(rename = "commitDate")]
@ -245,10 +255,6 @@ pub enum FrontendMessage {
id: NodeId,
value: String,
},
UpdateNodeTypes {
#[serde(rename = "nodeTypes")]
node_types: Vec<FrontendNodeType>,
},
UpdateOpenDocumentsList {
#[serde(rename = "openDocuments")]
open_documents: Vec<FrontendDocumentDetails>,

View file

@ -50,6 +50,9 @@ pub struct DocumentNodeDefinition {
/// Definition specific data. In order for the editor to access this data, the reference will be used.
pub category: &'static str,
pub properties: &'static (dyn Fn(&DocumentNode, NodeId, &mut NodePropertiesContext) -> Vec<LayoutGroup> + Sync),
/// User-facing description of the node's functionality.
pub description: Cow<'static, str>,
}
// We use the once cell for lazy initialization to avoid the overhead of reconstructing the node list every time.
@ -77,6 +80,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
..Default::default()
},
},
description: Cow::Borrowed("The identity node simply passes its data through. You can use this to organize your node graph if you want."),
properties: &|_document_node, _node_id, _context| node_properties::string_properties("The identity node simply passes its data through"),
},
// TODO: Auto-generate this from its proto node macro
@ -97,6 +101,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
..Default::default()
},
},
description: Cow::Borrowed("The Monitor node is used by the editor to access the data flowing through it"),
properties: &|_document_node, _node_id, _context| node_properties::string_properties("The Monitor node is used by the editor to access the data flowing through it"),
},
DocumentNodeDefinition {
@ -203,6 +208,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
..Default::default()
},
},
description: Cow::Borrowed("The Merge node combines graphical data through composition"),
properties: &node_properties::node_no_properties,
},
DocumentNodeDefinition {
@ -313,6 +319,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
..Default::default()
},
},
description: Cow::Borrowed("Creates a new Artboard which can be used as a working surface"),
properties: &node_properties::artboard_properties,
},
DocumentNodeDefinition {
@ -393,6 +400,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
..Default::default()
},
},
description: Cow::Borrowed("Loads an image from a given url."),
properties: &node_properties::load_image_properties,
},
DocumentNodeDefinition {
@ -457,6 +465,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
..Default::default()
},
},
description: Cow::Borrowed("Creates a new canvas object."),
properties: &node_properties::node_no_properties,
},
DocumentNodeDefinition {
@ -549,6 +558,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
..Default::default()
},
},
description: Cow::Borrowed("Draws raster data to a canvas element."),
properties: &node_properties::node_no_properties,
},
DocumentNodeDefinition {
@ -640,6 +650,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
..Default::default()
},
},
description: Cow::Borrowed("Rasterizes the given vector data"),
properties: &node_properties::rasterize_properties,
},
DocumentNodeDefinition {
@ -708,6 +719,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
..Default::default()
},
},
description: Cow::Borrowed("Creates an embedded image with the given transform"),
properties: &|_document_node, _node_id, _context| node_properties::string_properties("Creates an embedded image with the given transform"),
},
DocumentNodeDefinition {
@ -786,6 +798,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
..Default::default()
},
},
description: Cow::Borrowed("Generates different noise patters"),
properties: &node_properties::noise_pattern_properties,
},
// TODO: This needs to work with resolution-aware (raster with footprint, post-Cull node) data.
@ -808,6 +821,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
..Default::default()
},
},
description: Cow::Borrowed("TODO"),
properties: &node_properties::mask_properties,
},
// TODO: This needs to work with resolution-aware (raster with footprint, post-Cull node) data.
@ -831,6 +845,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
..Default::default()
},
},
description: Cow::Borrowed("TODO"),
properties: &node_properties::insert_channel_properties,
},
// TODO: This needs to work with resolution-aware (raster with footprint, post-Cull node) data.
@ -855,6 +870,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
..Default::default()
},
},
description: Cow::Borrowed("TODO"),
properties: &node_properties::node_no_properties,
},
DocumentNodeDefinition {
@ -968,6 +984,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
..Default::default()
},
},
description: Cow::Borrowed("TODO"),
properties: &node_properties::node_no_properties,
},
DocumentNodeDefinition {
@ -1036,6 +1053,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
..Default::default()
},
},
description: Cow::Borrowed("TODO"),
properties: &node_properties::node_no_properties,
},
DocumentNodeDefinition {
@ -1054,6 +1072,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
..Default::default()
},
},
description: Cow::Borrowed("TODO"),
properties: &node_properties::node_no_properties,
},
DocumentNodeDefinition {
@ -1072,6 +1091,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
..Default::default()
},
},
description: Cow::Borrowed("TODO"),
properties: &node_properties::node_no_properties,
},
DocumentNodeDefinition {
@ -1120,6 +1140,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
..Default::default()
},
},
description: Cow::Borrowed("TODO"),
properties: &|_document_node, _node_id, _context| node_properties::string_properties("A bitmap image embedded in this node"),
},
#[cfg(feature = "gpu")]
@ -1200,6 +1221,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
..Default::default()
},
},
description: Cow::Borrowed("TODO"),
properties: &node_properties::node_no_properties,
},
DocumentNodeDefinition {
@ -1278,6 +1300,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
..Default::default()
},
},
description: Cow::Borrowed("TODO"),
properties: &node_properties::node_no_properties,
},
DocumentNodeDefinition {
@ -1356,6 +1379,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
..Default::default()
},
},
description: Cow::Borrowed("TODO"),
properties: &node_properties::node_no_properties,
},
#[cfg(feature = "gpu")]
@ -1444,6 +1468,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
..Default::default()
},
},
description: Cow::Borrowed("TODO"),
properties: &node_properties::node_no_properties,
},
#[cfg(feature = "gpu")]
@ -1468,6 +1493,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
},
},
properties: &node_properties::node_no_properties,
description: Cow::Borrowed("TODO"),
},
#[cfg(feature = "gpu")]
DocumentNodeDefinition {
@ -1546,6 +1572,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
..Default::default()
},
},
description: Cow::Borrowed("TODO"),
properties: &node_properties::node_no_properties,
},
#[cfg(feature = "gpu")]
@ -1625,6 +1652,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
..Default::default()
},
},
description: Cow::Borrowed("TODO"),
properties: &node_properties::node_no_properties,
},
#[cfg(feature = "gpu")]
@ -1690,6 +1718,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
..Default::default()
},
},
description: Cow::Borrowed("TODO"),
properties: &node_properties::node_no_properties,
},
#[cfg(feature = "gpu")]
@ -1760,6 +1789,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
..Default::default()
},
},
description: Cow::Borrowed("TODO"),
properties: &node_properties::node_no_properties,
},
#[cfg(feature = "gpu")]
@ -1840,6 +1870,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
..Default::default()
},
},
description: Cow::Borrowed("TODO"),
properties: &node_properties::node_no_properties,
},
#[cfg(feature = "gpu")]
@ -1861,6 +1892,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
..Default::default()
},
},
description: Cow::Borrowed("TODO"),
properties: &node_properties::node_no_properties,
},
DocumentNodeDefinition {
@ -1878,6 +1910,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
..Default::default()
},
},
description: Cow::Borrowed("TODO"),
properties: &node_properties::node_no_properties,
},
DocumentNodeDefinition {
@ -1903,6 +1936,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
..Default::default()
},
},
description: Cow::Borrowed("TODO"),
properties: &node_properties::brightness_contrast_properties,
},
// Aims for interoperable compatibility with:
@ -1926,6 +1960,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
..Default::default()
},
},
description: Cow::Borrowed("TODO"),
properties: &node_properties::curves_properties,
},
(*IMAGINATE_NODE).clone(),
@ -1949,6 +1984,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
..Default::default()
},
},
description: Cow::Borrowed("TODO"),
properties: &node_properties::line_properties,
},
DocumentNodeDefinition {
@ -1971,6 +2007,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
..Default::default()
},
},
description: Cow::Borrowed("TODO"),
properties: &node_properties::spline_properties,
},
DocumentNodeDefinition {
@ -2041,6 +2078,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
..Default::default()
},
},
description: Cow::Borrowed("TODO"),
properties: &node_properties::node_no_properties,
},
DocumentNodeDefinition {
@ -2076,6 +2114,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
..Default::default()
},
},
description: Cow::Borrowed("TODO"),
properties: &node_properties::text_properties,
},
DocumentNodeDefinition {
@ -2163,6 +2202,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
},
},
properties: &node_properties::transform_properties,
description: Cow::Borrowed("TODO"),
},
DocumentNodeDefinition {
identifier: "Boolean Operation",
@ -2232,6 +2272,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
},
},
properties: &node_properties::boolean_operation_properties,
description: Cow::Borrowed("TODO"),
},
DocumentNodeDefinition {
identifier: "Copy to Points",
@ -2269,6 +2310,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
},
},
properties: &node_properties::copy_to_points_properties,
description: Cow::Borrowed("TODO"),
},
DocumentNodeDefinition {
identifier: "Sample Points",
@ -2365,6 +2407,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
},
},
properties: &node_properties::sample_points_properties,
description: Cow::Borrowed("TODO"),
},
DocumentNodeDefinition {
identifier: "Scatter Points",
@ -2437,6 +2480,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
},
},
properties: &node_properties::poisson_disk_points_properties,
description: Cow::Borrowed("TODO"),
},
// TODO: This needs to work with resolution-aware (raster with footprint, post-Cull node) data.
DocumentNodeDefinition {
@ -2455,6 +2499,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
},
},
properties: &node_properties::index_properties,
description: Cow::Borrowed("TODO"),
},
];
@ -2505,7 +2550,12 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
}
}
let NodeMetadata { display_name, category, fields } = metadata;
let NodeMetadata {
display_name,
category,
fields,
description,
} = 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 first_node_io = implementations.first().map(|(_, node_io)| node_io).unwrap_or(const { &NodeIOTypes::empty() });
@ -2582,6 +2632,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
},
},
category: category.unwrap_or("UNCATEGORIZED"),
description: Cow::Borrowed(description),
properties,
};
custom.push(node);
@ -2708,6 +2759,7 @@ pub static IMAGINATE_NODE: Lazy<DocumentNodeDefinition> = Lazy::new(|| DocumentN
},
},
properties: &node_properties::imaginate_properties,
description: Cow::Borrowed("TODO"),
});
pub fn resolve_document_node_type(identifier: &str) -> Option<&DocumentNodeDefinition> {
@ -2722,6 +2774,13 @@ pub fn collect_node_types() -> Vec<FrontendNodeType> {
.collect()
}
pub fn collect_node_descriptions() -> Vec<(String, String)> {
DOCUMENT_NODE_TYPES
.iter()
.map(|definition| (definition.identifier.to_string(), definition.description.to_string()))
.collect()
}
impl DocumentNodeDefinition {
/// Converts the [DocumentNodeDefinition] type to a [NodeTemplate], using the provided `input_override` and falling back to the default inputs.
/// `input_override` does not have to be the correct length.

View file

@ -1358,9 +1358,6 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
responses.add(BroadcastEvent::SelectionChanged);
responses.add(NodeGraphMessage::SendGraph);
let node_types = document_node_definitions::collect_node_types();
responses.add(FrontendMessage::UpdateNodeTypes { node_types });
}
NodeGraphMessage::UpdateTypes { resolved_types, node_graph_errors } => {
for (path, node_type) in resolved_types.add {
@ -1897,7 +1894,7 @@ impl NodeGraphMessageHandler {
.node_metadata(&node_id, breadcrumb_network_path)
.is_some_and(|node_metadata| node_metadata.persistent_metadata.is_layer()),
can_be_layer: can_be_layer_lookup.contains(&node_id),
reference: None,
reference: network_interface.reference(&node_id, breadcrumb_network_path),
display_name: network_interface.frontend_display_name(&node_id, breadcrumb_network_path),
primary_input,
exposed_inputs,

View file

@ -90,7 +90,7 @@
<TextLabel>{nodeCategory[0]}</TextLabel>
</summary>
{#each nodeCategory[1].nodes as nodeType}
<TextButton {disabled} label={nodeType.name} action={() => dispatch("selectNodeType", nodeType.name)} />
<TextButton {disabled} label={nodeType.name} tooltip={$nodeGraph.nodeDescriptions.get(nodeType.name)} action={() => dispatch("selectNodeType", nodeType.name)} />
{/each}
</details>
{:else}

View file

@ -461,6 +461,7 @@
{@const stackDataInput = node.exposedInputs[0]}
{@const layerAreaWidth = $nodeGraph.layerWidths.get(node.id) || 8}
{@const layerChainWidth = $nodeGraph.chainWidths.get(node.id) || 0}
{@const description = (node.reference && $nodeGraph.nodeDescriptions.get(node.reference)) || undefined}
<div
class="layer"
class:selected={$nodeGraph.selected.includes(node.id)}
@ -474,6 +475,7 @@
style:--data-color-dim={`var(--color-data-${(node.primaryOutput?.dataType || "General").toLowerCase()}-dim)`}
style:--layer-area-width={layerAreaWidth}
style:--node-chain-area-left-extension={layerChainWidth !== 0 ? layerChainWidth + 0.5 : 0}
title={description + (editor.handle.inDevelopmentMode() ? `\n\nNode ID: ${node.id}` : "")}
data-node={node.id}
bind:this={nodeElements[nodeIndex]}
>
@ -556,9 +558,7 @@
{/if}
<div class="details">
<!-- TODO: Allow the user to edit the name, just like in the Layers panel -->
<span title={editor.handle.inDevelopmentMode() ? `Node ID: ${node.id}` : undefined}>
{node.displayName}
</span>
<span>{node.displayName}</span>
</div>
<div class="solo-drag-grip" title="Drag only this layer without pushing others outside the stack"></div>
<IconButton
@ -604,6 +604,7 @@
{#each Array.from($nodeGraph.nodes.values()).flatMap((node, nodeIndex) => (node.isLayer ? [] : [{ node, nodeIndex }])) as { node, nodeIndex } (nodeIndex)}
{@const exposedInputsOutputs = [...node.exposedInputs, ...node.exposedOutputs]}
{@const clipPathId = String(Math.random()).substring(2)}
{@const description = (node.reference && $nodeGraph.nodeDescriptions.get(node.reference)) || undefined}
<div
class="node"
class:selected={$nodeGraph.selected.includes(node.id)}
@ -614,6 +615,7 @@
style:--clip-path-id={`url(#${clipPathId})`}
style:--data-color={`var(--color-data-${(node.primaryOutput?.dataType || "General").toLowerCase()})`}
style:--data-color-dim={`var(--color-data-${(node.primaryOutput?.dataType || "General").toLowerCase()}-dim)`}
title={description + (editor.handle.inDevelopmentMode() ? `\n\nNode ID: ${node.id}` : "")}
data-node={node.id}
bind:this={nodeElements[nodeIndex]}
>
@ -625,7 +627,7 @@
<div class="primary" class:in-selected-network={$nodeGraph.inSelectedNetwork} class:no-secondary-section={exposedInputsOutputs.length === 0}>
<IconLabel icon={nodeIcon(node.reference)} />
<!-- TODO: Allow the user to edit the name, just like in the Layers panel -->
<TextLabel tooltip={editor.handle.inDevelopmentMode() ? `Node ID: ${node.id}` : undefined}>{node.displayName}</TextLabel>
<TextLabel>{node.displayName}</TextLabel>
</div>
<!-- Secondary rows -->
{#if exposedInputsOutputs.length > 0}

View file

@ -10,6 +10,7 @@ import {
type FrontendNodeWire as FrontendNodeWire,
type FrontendNodeType,
type WirePath,
SendUIMetadata,
UpdateBox,
UpdateClickTargets,
UpdateContextMenuInformation,
@ -19,7 +20,6 @@ import {
UpdateNodeGraph,
UpdateNodeGraphSelection,
UpdateNodeGraphTransform,
UpdateNodeTypes,
UpdateNodeThumbnail,
UpdateWirePathInProgress,
UpdateZoomWithScroll,
@ -38,6 +38,8 @@ export function createNodeGraphState(editor: Editor) {
nodes: new Map<bigint, FrontendNode>(),
wires: [] as FrontendNodeWire[],
wirePathInProgress: undefined as WirePath | undefined,
inputTypeDescriptions: new Map<string, string>(),
nodeDescriptions: new Map<string, string>(),
nodeTypes: [] as FrontendNodeType[],
zoomWithScroll: false as boolean,
thumbnails: new Map<bigint, string>(),
@ -47,6 +49,14 @@ export function createNodeGraphState(editor: Editor) {
});
// Set up message subscriptions on creation
editor.subscriptions.subscribeJsMessage(SendUIMetadata, (UIMetadata) => {
update((state) => {
state.inputTypeDescriptions = UIMetadata.inputTypeDescriptions;
state.nodeDescriptions = UIMetadata.nodeDescriptions;
state.nodeTypes = UIMetadata.nodeTypes;
return state;
});
});
editor.subscriptions.subscribeJsMessage(UpdateBox, (updateBox) => {
update((state) => {
state.box = updateBox.box;
@ -108,12 +118,6 @@ export function createNodeGraphState(editor: Editor) {
return state;
});
});
editor.subscriptions.subscribeJsMessage(UpdateNodeTypes, (updateNodeTypes) => {
update((state) => {
state.nodeTypes = updateNodeTypes.nodeTypes;
return state;
});
});
editor.subscriptions.subscribeJsMessage(UpdateNodeThumbnail, (updateNodeThumbnail) => {
update((state) => {
state.thumbnails.set(updateNodeThumbnail.id, updateNodeThumbnail.value);

View file

@ -89,7 +89,14 @@ export class UpdateNodeGraphTransform extends JsMessage {
readonly transform!: NodeGraphTransform;
}
export class UpdateNodeTypes extends JsMessage {
const InputTypeDescriptions = Transform(({ obj }) => new Map(obj.inputTypeDescriptions));
const NodeDescriptions = Transform(({ obj }) => new Map(obj.nodeDescriptions));
export class SendUIMetadata extends JsMessage {
@InputTypeDescriptions
readonly inputTypeDescriptions!: Map<string, string>;
@NodeDescriptions
readonly nodeDescriptions!: Map<string, string>;
@Type(() => FrontendNode)
readonly nodeTypes!: FrontendNodeType[];
}
@ -1547,6 +1554,7 @@ export const messageMakers: Record<string, MessageMaker> = {
DisplayEditableTextbox,
DisplayEditableTextboxTransform,
DisplayRemoveEditableTextbox,
SendUIMetadata,
TriggerAboutGraphiteLocalizedCommitDate,
TriggerCopyToClipboardBlobUrl,
TriggerDelayedZoomCanvasToFitAll,
@ -1596,7 +1604,6 @@ export const messageMakers: Record<string, MessageMaker> = {
UpdateNodeGraphSelection,
UpdateNodeGraphTransform,
UpdateNodeThumbnail,
UpdateNodeTypes,
UpdateOpenDocumentsList,
UpdatePropertyPanelSectionsLayout,
UpdateToolOptionsLayout,

View file

@ -34,6 +34,7 @@ pub struct NodeMetadata {
pub display_name: &'static str,
pub category: Option<&'static str>,
pub fields: Vec<FieldMetadata>,
pub description: &'static str,
}
#[derive(Clone, Debug)]

View file

@ -500,6 +500,7 @@ fn modify_existing() {
);
}
// Do we want to enforce that all serialized/deserialized hashmaps are a vec of tuples?
// TODO: Eventually remove this (probably starting late 2024)
use serde::de::{SeqAccess, Visitor};
use serde::ser::SerializeSeq;

View file

@ -22,6 +22,7 @@ pub(crate) fn generate_node_code(parsed: &ParsedNodeFn) -> syn::Result<TokenStre
fields,
body,
crate_name: graphene_core_crate,
description,
..
} = parsed;
@ -257,6 +258,7 @@ pub(crate) fn generate_node_code(parsed: &ParsedNodeFn) -> syn::Result<TokenStre
let metadata = NodeMetadata {
display_name: #display_name,
category: #category,
description: #description,
fields: vec![
#(
FieldMetadata {

View file

@ -5,7 +5,7 @@ use quote::{format_ident, ToTokens};
use syn::parse::{Parse, ParseStream, Parser};
use syn::punctuated::Punctuated;
use syn::token::{Comma, RArrow};
use syn::{Attribute, Error, ExprTuple, FnArg, GenericParam, Ident, ItemFn, LitFloat, LitStr, Meta, Pat, PatIdent, PatType, Path, ReturnType, Type, WhereClause};
use syn::{AttrStyle, Attribute, Error, Expr, ExprTuple, FnArg, GenericParam, Ident, ItemFn, Lit, LitFloat, LitStr, Meta, Pat, PatIdent, PatType, Path, ReturnType, Type, WhereClause};
use crate::codegen::generate_node_code;
@ -30,6 +30,7 @@ pub(crate) struct ParsedNodeFn {
pub(crate) fields: Vec<ParsedField>,
pub(crate) body: TokenStream2,
pub(crate) crate_name: proc_macro_crate::FoundCrate,
pub(crate) description: String,
}
#[derive(Debug, Default)]
@ -208,6 +209,22 @@ fn parse_node_fn(attr: TokenStream2, item: TokenStream2) -> syn::Result<ParsedNo
format!("Failed to find location of graphene_core. Make sure it is imported as a dependency: {}", e),
)
})?;
let description = input_fn
.attrs
.iter()
.filter_map(|a| {
if a.style != AttrStyle::Outer {
return None;
}
let Meta::NameValue(name_val) = &a.meta else { return None };
if name_val.path.get_ident().map(|x| x.to_string()) != Some("doc".into()) {
return None;
}
let Expr::Lit(expr_lit) = &name_val.value else { return None };
let Lit::Str(ref text) = expr_lit.lit else { return None };
Some(text.value().trim().to_string())
})
.fold(String::new(), |acc, b| acc + &b + "\n");
Ok(ParsedNodeFn {
attributes,
@ -222,6 +239,7 @@ fn parse_node_fn(attr: TokenStream2, item: TokenStream2) -> syn::Result<ParsedNo
where_clause,
body,
crate_name,
description,
})
}
@ -490,6 +508,7 @@ mod tests {
assert_eq!(parsed.attributes.path, expected.attributes.path);
assert_eq!(parsed.attributes.skip_impl, expected.attributes.skip_impl);
assert_eq!(parsed.fields.len(), expected.fields.len());
assert_eq!(parsed.description, expected.description);
for (parsed_field, expected_field) in parsed.fields.iter().zip(expected.fields.iter()) {
match (parsed_field, expected_field) {
@ -550,6 +569,8 @@ mod tests {
fn test_basic_node() {
let attr = quote!(category("Math: Arithmetic"), path(graphene_core::TestNode), skip_impl);
let input = quote!(
/// Multi
/// Line
fn add(a: f64, b: f64) -> f64 {
a + b
}
@ -588,6 +609,7 @@ mod tests {
}],
body: TokenStream2::new(),
crate_name: FoundCrate::Itself,
description: String::from("Multi\nLine\n"),
};
assert_parsed_node_fn(&parsed, &expected);
@ -597,6 +619,10 @@ mod tests {
fn test_node_with_impl_node() {
let attr = quote!(category("General"));
let input = quote!(
/**
Hello
World
*/
fn transform<T: 'static>(footprint: Footprint, transform_target: impl Node<Footprint, Output = T>, translate: DVec2) -> T {
// Implementation details...
}
@ -644,6 +670,7 @@ mod tests {
],
body: TokenStream2::new(),
crate_name: FoundCrate::Itself,
description: String::from("Hello\n\t\t\t\tWorld\n"),
};
assert_parsed_node_fn(&parsed, &expected);
@ -653,6 +680,7 @@ mod tests {
fn test_node_with_default_values() {
let attr = quote!(category("Vector: Shape"));
let input = quote!(
/// Test
fn circle(_: (), #[default(50.)] radius: f64) -> VectorData {
// Implementation details...
}
@ -691,6 +719,7 @@ mod tests {
}],
body: TokenStream2::new(),
crate_name: FoundCrate::Itself,
description: "Test\n".into(),
};
assert_parsed_node_fn(&parsed, &expected);
@ -743,6 +772,7 @@ mod tests {
}],
body: TokenStream2::new(),
crate_name: FoundCrate::Itself,
description: String::new(),
};
assert_parsed_node_fn(&parsed, &expected);
@ -796,6 +826,7 @@ mod tests {
}],
body: TokenStream2::new(),
crate_name: FoundCrate::Itself,
description: String::new(),
};
assert_parsed_node_fn(&parsed, &expected);
@ -843,6 +874,7 @@ mod tests {
}],
body: TokenStream2::new(),
crate_name: FoundCrate::Itself,
description: String::new(),
};
assert_parsed_node_fn(&parsed, &expected);
@ -880,6 +912,7 @@ mod tests {
fields: vec![],
body: TokenStream2::new(),
crate_name: FoundCrate::Itself,
description: String::new(),
};
assert_parsed_node_fn(&parsed, &expected);