From 9173901c7ace0eafac33c5b3c30a1c0d20a5d823 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Sat, 2 May 2026 23:19:09 -0700 Subject: [PATCH 1/2] New nodes: 'Zip Attribute' and 'Read Attribute {Vector, Number, Bool, String, Transform, Color, Blend Mode, Gradient Type, Spread Method} --- .../data_panel/data_panel_message_handler.rs | 55 +++- .../interpreted-executor/src/node_registry.rs | 10 + node-graph/libraries/core-types/src/table.rs | 12 +- node-graph/nodes/graphic/src/graphic.rs | 236 +++++++++++++++++- 4 files changed, 305 insertions(+), 8 deletions(-) diff --git a/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs b/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs index fea4a9e816..2605460e88 100644 --- a/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs +++ b/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs @@ -12,7 +12,7 @@ use graphene_std::memo::IORecord; use graphene_std::raster_types::{CPU, GPU, Raster}; use graphene_std::table::Table; use graphene_std::vector::Vector; -use graphene_std::vector::style::{Fill, FillChoice}; +use graphene_std::vector::style::{Fill, FillChoice, GradientSpreadMethod, GradientType}; use graphene_std::{Artboard, Color, Context, Graphic}; use std::any::Any; use std::sync::Arc; @@ -191,6 +191,11 @@ fn generate_layout(introspected_data: &Arc, Table, Table, + Table, + Table, + Table, + Table, + Table, GradientStops, f64, u32, @@ -200,6 +205,9 @@ fn generate_layout(introspected_data: &Arc, DVec2, DAffine2, + BlendMode, + GradientType, + GradientSpreadMethod, ]) } @@ -757,6 +765,51 @@ impl TableRowLayout for Affine2 { } } +impl TableRowLayout for BlendMode { + fn type_name() -> &'static str { + "BlendMode" + } + fn identifier(&self) -> String { + self.to_string() + } + fn value_widget(&self, _target: PathStep, _data: &LayoutData) -> WidgetInstance { + TextLabel::new(self.to_string()).narrow(true).widget_instance() + } + fn value_page(&self, _data: &mut LayoutData) -> Vec { + vec![LayoutGroup::row(vec![self.value_widget(PathStep::Element(0), _data)])] + } +} + +impl TableRowLayout for GradientType { + fn type_name() -> &'static str { + "GradientType" + } + fn identifier(&self) -> String { + format!("{self:?}") + } + fn value_widget(&self, _target: PathStep, _data: &LayoutData) -> WidgetInstance { + TextLabel::new(format!("{self:?}")).narrow(true).widget_instance() + } + fn value_page(&self, _data: &mut LayoutData) -> Vec { + vec![LayoutGroup::row(vec![self.value_widget(PathStep::Element(0), _data)])] + } +} + +impl TableRowLayout for GradientSpreadMethod { + fn type_name() -> &'static str { + "GradientSpreadMethod" + } + fn identifier(&self) -> String { + format!("{self:?}") + } + fn value_widget(&self, _target: PathStep, _data: &LayoutData) -> WidgetInstance { + TextLabel::new(format!("{self:?}")).narrow(true).widget_instance() + } + fn value_page(&self, _data: &mut LayoutData) -> Vec { + vec![LayoutGroup::row(vec![self.value_widget(PathStep::Element(0), _data)])] + } +} + /// Resolves the value/breadcrumb label for a `NodeId` against `network_interface` at the given `network_path`, /// falling back to "Node {id}" if the node isn't present (e.g. an ID that no longer maps to a real node). fn node_id_display_label(node_id: NodeId, network_interface: &NodeNetworkInterface, network_path: &[NodeId]) -> String { diff --git a/node-graph/interpreted-executor/src/node_registry.rs b/node-graph/interpreted-executor/src/node_registry.rs index 9af0bc21e3..8689848cb6 100644 --- a/node-graph/interpreted-executor/src/node_registry.rs +++ b/node-graph/interpreted-executor/src/node_registry.rs @@ -99,6 +99,11 @@ fn node_registry() -> HashMap, input: Context, fn_params: [Context => Table]), async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Table]), async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Table]), + async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Table]), + async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Table]), + async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Table]), + async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Table]), + async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Table]), async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Graphic]), async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::text::Font]), async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Table]), @@ -156,6 +161,11 @@ fn node_registry() -> HashMap, input: Context, fn_params: [Context => Table]), async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Table]), async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Table]), + async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Table]), + async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Table]), + async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Table]), + async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Table]), + async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Table]), #[cfg(target_family = "wasm")] async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => CanvasHandle]), async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => f64]), diff --git a/node-graph/libraries/core-types/src/table.rs b/node-graph/libraries/core-types/src/table.rs index 2dd6dd8f50..e4b119c2db 100644 --- a/node-graph/libraries/core-types/src/table.rs +++ b/node-graph/libraries/core-types/src/table.rs @@ -24,7 +24,7 @@ pub const ATTR_OPACITY: &str = "opacity"; /// Like opacity but does not affect content clipped to the row. pub const ATTR_OPACITY_FILL: &str = "opacity_fill"; -/// Whether a row inherits the alpha of the content beneath it (clipping mask). +/// `bool` for whether a row inherits the alpha of the content beneath it (clipping mask). pub const ATTR_CLIPPING_MASK: &str = "clipping_mask"; /// `Table` path from the root network to the layer node owning this row. @@ -47,16 +47,16 @@ pub const ATTR_EDITOR_CLICK_TARGET: &str = "editor:click_target"; /// its drag cage. Stored as an affine to allow non-axis-aligned frames in the future. pub const ATTR_EDITOR_TEXT_FRAME: &str = "editor:text_frame"; -/// Byte offset where a regex match begins ('Regex Find All', 'Regex Capture' text nodes). +/// `u64` byte offset where a regex match begins ('Regex Find All', 'Regex Capture' text nodes). pub const ATTR_START: &str = "start"; -/// Byte offset where a regex match ends ('Regex Find All', 'Regex Capture' text nodes). +/// `u64` byte offset where a regex match ends ('Regex Find All', 'Regex Capture' text nodes). pub const ATTR_END: &str = "end"; -/// Regex named-capture-group's name, or empty for unnamed groups ('Regex Capture' text node). +/// `String` for a regex named-capture-group's name, or empty for unnamed groups ('Regex Capture' text node). pub const ATTR_NAME: &str = "name"; -/// JSON value's type string (`"string"`, `"number"`, `"object"`, etc.) from 'JSON Query All'. +/// `String` for a JSON value's type (`"string"`, `"number"`, `"object"`, etc.) from 'JSON Query All'. pub const ATTR_TYPE: &str = "type"; /// Artboard's `DVec2` top-left corner in document coordinates. @@ -68,7 +68,7 @@ pub const ATTR_DIMENSIONS: &str = "dimensions"; /// Artboard's `Color` background fill. pub const ATTR_BACKGROUND: &str = "background"; -/// Whether an artboard clips content to its bounds. +/// `bool` for whether an artboard clips content to its bounds. pub const ATTR_CLIP: &str = "clip"; /// Gradient's `GradientSpreadMethod` (`Pad`, `Reflect`, or `Repeat`). diff --git a/node-graph/nodes/graphic/src/graphic.rs b/node-graph/nodes/graphic/src/graphic.rs index 45e1670c29..140e741043 100644 --- a/node-graph/nodes/graphic/src/graphic.rs +++ b/node-graph/nodes/graphic/src/graphic.rs @@ -2,11 +2,12 @@ use core_types::bounds::{BoundingBox, RenderBoundingBox}; use core_types::registry::types::{Angle, SignedInteger}; use core_types::table::{Table, TableRow}; use core_types::uuid::NodeId; -use core_types::{ATTR_EDITOR_LAYER_PATH, ATTR_TRANSFORM, AnyHash, CacheHash, CloneVarArgs, Color, Context, Ctx, ExtractAll, OwnedContextImpl}; +use core_types::{ATTR_EDITOR_LAYER_PATH, ATTR_TRANSFORM, AnyHash, BlendMode, CacheHash, CloneVarArgs, Color, Context, Ctx, ExtractAll, OwnedContextImpl}; use glam::{DAffine2, DVec2}; use graphic_types::graphic::{Graphic, IntoGraphicTable}; use graphic_types::{Artboard, Vector}; use raster_types::{CPU, GPU, Raster}; +use vector_types::gradient::{GradientSpreadMethod, GradientType}; use vector_types::{GradientStop, GradientStops, ReferencePoint}; /// Returns the value at the specified index in the list. @@ -257,6 +258,239 @@ async fn write_attribute( + _: impl Ctx, + /// The `Table` to attach the new attribute to. + #[implementations( + Table, Table, Table, Table, Table, Table, Table, Table, Table, Table, Table, Table, Table, + Table, Table, Table, Table, Table, Table, Table, Table, Table, Table, Table, Table, Table, + Table, Table, Table, Table, Table, Table, Table, Table, Table, Table, Table, Table, Table, + Table>, Table>, Table>, Table>, Table>, Table>, Table>, Table>, Table>, Table>, Table>, Table>, Table>, + Table, Table, Table, Table, Table, Table, Table, Table, Table, Table, Table, Table, Table, + Table, Table, Table, Table, Table, Table, Table, Table, Table, Table, Table, Table, Table, + Table, Table, Table, Table, Table, Table, Table, Table, Table, Table, Table, Table, Table, + Table, Table, Table, Table, Table, Table, Table, Table, Table, Table, Table, Table, Table, + Table, Table, Table, Table, Table, Table, Table, Table, Table, Table, Table, Table, Table, + Table, Table, Table, Table, Table, Table, Table, Table, Table, Table, Table, Table, Table, + Table, Table, Table, Table, Table, Table, Table, Table, Table, Table, Table, Table, Table, + Table, Table, Table, Table, Table, Table, Table, Table, Table, Table, Table, Table, Table, + Table, Table, Table, Table, Table, Table, Table, Table, Table, Table, Table, Table, Table, + )] + mut content: Table, + /// The table to draw the element values from. + #[expose] + #[implementations( + Table, Table, Table, Table>, Table, Table, Table, Table, Table, Table, Table, Table, Table, + Table, Table, Table, Table>, Table, Table, Table, Table, Table, Table, Table, Table, Table, + Table, Table, Table, Table>, Table, Table, Table, Table, Table, Table, Table, Table, Table, + Table, Table, Table, Table>, Table, Table, Table, Table, Table, Table, Table, Table, Table, + Table, Table, Table, Table>, Table, Table, Table, Table, Table, Table, Table, Table, Table, + Table, Table, Table, Table>, Table, Table, Table, Table, Table, Table, Table, Table, Table, + Table, Table, Table, Table>, Table, Table, Table, Table, Table, Table, Table, Table, Table, + Table, Table, Table, Table>, Table, Table, Table, Table, Table, Table, Table, Table, Table, + Table, Table, Table, Table>, Table, Table, Table, Table, Table, Table, Table, Table, Table, + Table, Table, Table, Table>, Table, Table, Table, Table, Table, Table, Table, Table, Table, + Table, Table, Table, Table>, Table, Table, Table, Table, Table, Table, Table, Table, Table, + Table, Table, Table, Table>, Table, Table, Table, Table, Table, Table, Table, Table, Table, + Table, Table, Table, Table>, Table, Table, Table, Table, Table, Table, Table, Table, Table, + )] + source: Table, + /// The name to assign to the new destination attribute. + name: String, +) -> Table { + if source.is_empty() { + return content; + } + for index in 0..content.len() { + let Some(value) = source.element(index % source.len()).cloned() else { continue }; + content.set_attribute(&name, index, value); + } + content +} + +/// Reads a named `Vector` attribute from each row of the input table, outputting a new table where each row's element is the attribute value. Rows without the attribute are omitted. +#[node_macro::node(category("General"))] +fn read_attribute_vector( + _: impl Ctx, + #[implementations( + Table, Table, Table, Table>, Table, Table, + Table, Table, Table, Table, Table, Table, Table, + )] + content: Table, + /// The attribute name (key) to read from each row. + name: String, +) -> Table { + let mut result = Table::with_capacity(content.len()); + for index in 0..content.len() { + let Some(value) = content.attribute::(&name, index) else { continue }; + result.push(TableRow::new_from_element(value.clone())); + } + result +} + +/// Reads a named numeric attribute (`f64` or `u64`) from each row of the input table, outputting a new `Table`. `u64` values are converted to `f64`. Rows without the attribute are omitted. +#[node_macro::node(category("General"))] +fn read_attribute_number( + _: impl Ctx, + #[implementations( + Table, Table, Table, Table>, Table, Table, + Table, Table, Table, Table, Table, Table, Table, + )] + content: Table, + /// The attribute name (key) to read from each row. + name: String, +) -> Table { + let mut result = Table::with_capacity(content.len()); + for index in 0..content.len() { + let value = content.attribute::(&name, index).copied().or_else(|| content.attribute::(&name, index).map(|v| *v as f64)); + let Some(value) = value else { continue }; + result.push(TableRow::new_from_element(value)); + } + result +} + +/// Reads a named `bool` attribute from each row of the input table, outputting a new `Table`. Rows without the attribute are omitted. +#[node_macro::node(category("General"))] +fn read_attribute_bool( + _: impl Ctx, + #[implementations( + Table, Table, Table, Table>, Table, Table, + Table, Table, Table, Table, Table, Table, Table, + )] + content: Table, + /// The attribute name (key) to read from each row. + name: String, +) -> Table { + let mut result = Table::with_capacity(content.len()); + for index in 0..content.len() { + let Some(value) = content.attribute::(&name, index) else { continue }; + result.push(TableRow::new_from_element(*value)); + } + result +} + +/// Reads a named `String` attribute from each row of the input table, outputting a new `Table`. Rows without the attribute are omitted. +#[node_macro::node(category("General"))] +fn read_attribute_string( + _: impl Ctx, + #[implementations( + Table, Table, Table, Table>, Table, Table, + Table, Table, Table, Table, Table, Table, Table, + )] + content: Table, + /// The attribute name (key) to read from each row. + name: String, +) -> Table { + let mut result = Table::with_capacity(content.len()); + for index in 0..content.len() { + let Some(value) = content.attribute::(&name, index) else { continue }; + result.push(TableRow::new_from_element(value.clone())); + } + result +} + +/// Reads a named `DAffine2` transform attribute from each row of the input table, outputting a new `Table`. Rows without the attribute are omitted. +#[node_macro::node(category("General"))] +fn read_attribute_transform( + _: impl Ctx, + #[implementations( + Table, Table, Table, Table>, Table, Table, + Table, Table, Table, Table, Table, Table, Table, + )] + content: Table, + /// The attribute name (key) to read from each row. + name: String, +) -> Table { + let mut result = Table::with_capacity(content.len()); + for index in 0..content.len() { + let Some(value) = content.attribute::(&name, index) else { continue }; + result.push(TableRow::new_from_element(*value)); + } + result +} + +/// Reads a named `Color` attribute from each row of the input table, outputting a new `Table`. Rows without the attribute are omitted. +#[node_macro::node(category("General"))] +fn read_attribute_color( + _: impl Ctx, + #[implementations( + Table, Table, Table, Table>, Table, Table, + Table, Table, Table, Table, Table, Table, Table, + )] + content: Table, + /// The attribute name (key) to read from each row. + name: String, +) -> Table { + let mut result = Table::with_capacity(content.len()); + for index in 0..content.len() { + let Some(value) = content.attribute::(&name, index) else { continue }; + result.push(TableRow::new_from_element(*value)); + } + result +} + +/// Reads a named `BlendMode` attribute from each row of the input table, outputting a new `Table`. Rows without the attribute are omitted. +#[node_macro::node(category("General"))] +fn read_attribute_blend_mode( + _: impl Ctx, + #[implementations( + Table, Table, Table, Table>, Table, Table, + Table, Table, Table, Table, Table, Table, Table, + )] + content: Table, + /// The attribute name (key) to read from each row. + name: String, +) -> Table { + let mut result = Table::with_capacity(content.len()); + for index in 0..content.len() { + let Some(value) = content.attribute::(&name, index) else { continue }; + result.push(TableRow::new_from_element(*value)); + } + result +} + +/// Reads a named `GradientType` attribute from each row of the input table, outputting a new `Table`. Rows without the attribute are omitted. +#[node_macro::node(category("General"))] +fn read_attribute_gradient_type( + _: impl Ctx, + #[implementations( + Table, Table, Table, Table>, Table, Table, + Table, Table, Table, Table, Table, Table, Table, + )] + content: Table, + /// The attribute name (key) to read from each row. + name: String, +) -> Table { + let mut result = Table::with_capacity(content.len()); + for index in 0..content.len() { + let Some(value) = content.attribute::(&name, index) else { continue }; + result.push(TableRow::new_from_element(*value)); + } + result +} + +/// Reads a named `GradientSpreadMethod` attribute from each row of the input table, outputting a new `Table`. Rows without the attribute are omitted. +#[node_macro::node(category("General"))] +fn read_attribute_spread_method( + _: impl Ctx, + #[implementations( + Table, Table, Table, Table>, Table, Table, + Table, Table, Table, Table, Table, Table, Table, + )] + content: Table, + /// The attribute name (key) to read from each row. + name: String, +) -> Table { + let mut result = Table::with_capacity(content.len()); + for index in 0..content.len() { + let Some(value) = content.attribute::(&name, index) else { continue }; + result.push(TableRow::new_from_element(*value)); + } + result +} + /// Joins two `Table`s of the same type, extending the base `Table` with the items from the new `Table`. #[node_macro::node(category("General"))] pub async fn extend( From 19c49a54537b2db0a6e78d7ce9e32826435aaebc Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Sun, 3 May 2026 00:11:05 -0700 Subject: [PATCH 2/2] Cleanup --- .../data_panel/data_panel_message_handler.rs | 11 ++- node-graph/nodes/graphic/src/graphic.rs | 78 ++++++++++--------- 2 files changed, 48 insertions(+), 41 deletions(-) diff --git a/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs b/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs index 2605460e88..41c6048e57 100644 --- a/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs +++ b/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs @@ -785,10 +785,10 @@ impl TableRowLayout for GradientType { "GradientType" } fn identifier(&self) -> String { - format!("{self:?}") + self.to_string() } fn value_widget(&self, _target: PathStep, _data: &LayoutData) -> WidgetInstance { - TextLabel::new(format!("{self:?}")).narrow(true).widget_instance() + TextLabel::new(self.to_string()).narrow(true).widget_instance() } fn value_page(&self, _data: &mut LayoutData) -> Vec { vec![LayoutGroup::row(vec![self.value_widget(PathStep::Element(0), _data)])] @@ -800,10 +800,10 @@ impl TableRowLayout for GradientSpreadMethod { "GradientSpreadMethod" } fn identifier(&self) -> String { - format!("{self:?}") + self.to_string() } fn value_widget(&self, _target: PathStep, _data: &LayoutData) -> WidgetInstance { - TextLabel::new(format!("{self:?}")).narrow(true).widget_instance() + TextLabel::new(self.to_string()).narrow(true).widget_instance() } fn value_page(&self, _data: &mut LayoutData) -> Vec { vec![LayoutGroup::row(vec![self.value_widget(PathStep::Element(0), _data)])] @@ -974,6 +974,9 @@ macro_rules! known_table_row_types { /// Uses `Display` instead of `Debug` for attribute types that have a nicer human-readable format. fn display_value_override(any: &dyn Any) -> Option { + if let Some(value) = any.downcast_ref::() { + return Some(format_dvec2(*value)); + } if let Some(value) = any.downcast_ref::() { return Some(value.to_string()); } diff --git a/node-graph/nodes/graphic/src/graphic.rs b/node-graph/nodes/graphic/src/graphic.rs index 140e741043..4a20645aea 100644 --- a/node-graph/nodes/graphic/src/graphic.rs +++ b/node-graph/nodes/graphic/src/graphic.rs @@ -217,14 +217,14 @@ pub fn path_of_subgraph(_: impl Ctx, node_path: Table) -> Table node_path.into_iter().take(len.saturating_sub(1)).collect() } -/// Writes a named attribute on each item of the input `Table`. The value-producing input is evaluated once per item, -/// with the item's index and the item itself (as a `Table` containing only that item, passed as a vararg) provided via -/// context, so the upstream pipeline can return a different value per item that may be derived from the item's own data. -/// If the attribute already exists, its values are replaced; if not, the attribute is added. -#[node_macro::node(category("General"))] +/// Sets a named attribute on the input `Table`, computing one value per item via the value-producing input. That input +/// is evaluated once per item, with the item's index and the item itself (as a `Table` containing only that item, +/// passed as a vararg) provided via context, so the upstream pipeline can return a different value per item that may +/// be derived from the item's own data. If the attribute already exists, its values are replaced; if not, it's added. +#[node_macro::node(category("Attributes: Write"))] async fn write_attribute( ctx: impl ExtractAll + CloneVarArgs + Ctx, - /// The `Table` whose items will gain or have replaced the named attribute. + /// The `Table` to set the named attribute on (one value per item). #[implementations( Table, Table, Table, Table, Table, Table, Table, Table, Table, Table, Table, Table, Table, Table, Table, Table, Table, Table, Table, Table, @@ -258,9 +258,9 @@ async fn write_attribute( +/// Sets a named attribute on the primary table, with each value taken from the corresponding item's element in the source table (paired by index, wrapping if the source has fewer items). +#[node_macro::node(category("Attributes: Write"))] +fn attach_attribute( _: impl Ctx, /// The `Table` to attach the new attribute to. #[implementations( @@ -310,8 +310,8 @@ fn zip_attribute`. +#[node_macro::node(category("Attributes: Read"))] fn read_attribute_vector( _: impl Ctx, #[implementations( @@ -319,7 +319,7 @@ fn read_attribute_vector( Table, Table, Table, Table, Table, Table, Table, )] content: Table, - /// The attribute name (key) to read from each row. + /// The attribute name (key) to read. name: String, ) -> Table { let mut result = Table::with_capacity(content.len()); @@ -330,8 +330,8 @@ fn read_attribute_vector( result } -/// Reads a named numeric attribute (`f64` or `u64`) from each row of the input table, outputting a new `Table`. `u64` values are converted to `f64`. Rows without the attribute are omitted. -#[node_macro::node(category("General"))] +/// Reads a named numeric attribute (`f64`, `u64`, or `u32`) from the input table, outputting each value as an element of a new `Table`. Integer values are converted to `f64`. +#[node_macro::node(category("Attributes: Read"))] fn read_attribute_number( _: impl Ctx, #[implementations( @@ -339,20 +339,24 @@ fn read_attribute_number( Table, Table, Table, Table, Table, Table, Table, )] content: Table, - /// The attribute name (key) to read from each row. + /// The attribute name (key) to read. name: String, ) -> Table { let mut result = Table::with_capacity(content.len()); for index in 0..content.len() { - let value = content.attribute::(&name, index).copied().or_else(|| content.attribute::(&name, index).map(|v| *v as f64)); + let value = content + .attribute::(&name, index) + .copied() + .or_else(|| content.attribute::(&name, index).map(|v| *v as f64)) + .or_else(|| content.attribute::(&name, index).map(|v| *v as f64)); let Some(value) = value else { continue }; result.push(TableRow::new_from_element(value)); } result } -/// Reads a named `bool` attribute from each row of the input table, outputting a new `Table`. Rows without the attribute are omitted. -#[node_macro::node(category("General"))] +/// Reads a named `bool` attribute from the input table, outputting each value as an element of a new `Table`. +#[node_macro::node(category("Attributes: Read"))] fn read_attribute_bool( _: impl Ctx, #[implementations( @@ -360,7 +364,7 @@ fn read_attribute_bool( Table, Table, Table, Table, Table, Table, Table, )] content: Table, - /// The attribute name (key) to read from each row. + /// The attribute name (key) to read. name: String, ) -> Table { let mut result = Table::with_capacity(content.len()); @@ -371,8 +375,8 @@ fn read_attribute_bool( result } -/// Reads a named `String` attribute from each row of the input table, outputting a new `Table`. Rows without the attribute are omitted. -#[node_macro::node(category("General"))] +/// Reads a named `String` attribute from the input table, outputting each value as an element of a new `Table`. +#[node_macro::node(category("Attributes: Read"))] fn read_attribute_string( _: impl Ctx, #[implementations( @@ -380,7 +384,7 @@ fn read_attribute_string( Table, Table, Table, Table, Table, Table, Table, )] content: Table, - /// The attribute name (key) to read from each row. + /// The attribute name (key) to read. name: String, ) -> Table { let mut result = Table::with_capacity(content.len()); @@ -391,8 +395,8 @@ fn read_attribute_string( result } -/// Reads a named `DAffine2` transform attribute from each row of the input table, outputting a new `Table`. Rows without the attribute are omitted. -#[node_macro::node(category("General"))] +/// Reads a named `DAffine2` transform attribute from the input table, outputting each value as an element of a new `Table`. +#[node_macro::node(category("Attributes: Read"))] fn read_attribute_transform( _: impl Ctx, #[implementations( @@ -400,7 +404,7 @@ fn read_attribute_transform( Table, Table, Table, Table, Table, Table, Table, )] content: Table, - /// The attribute name (key) to read from each row. + /// The attribute name (key) to read. name: String, ) -> Table { let mut result = Table::with_capacity(content.len()); @@ -411,8 +415,8 @@ fn read_attribute_transform( result } -/// Reads a named `Color` attribute from each row of the input table, outputting a new `Table`. Rows without the attribute are omitted. -#[node_macro::node(category("General"))] +/// Reads a named `Color` attribute from the input table, outputting each value as an element of a new `Table`. +#[node_macro::node(category("Attributes: Read"))] fn read_attribute_color( _: impl Ctx, #[implementations( @@ -420,7 +424,7 @@ fn read_attribute_color( Table, Table, Table, Table, Table, Table, Table, )] content: Table, - /// The attribute name (key) to read from each row. + /// The attribute name (key) to read. name: String, ) -> Table { let mut result = Table::with_capacity(content.len()); @@ -431,8 +435,8 @@ fn read_attribute_color( result } -/// Reads a named `BlendMode` attribute from each row of the input table, outputting a new `Table`. Rows without the attribute are omitted. -#[node_macro::node(category("General"))] +/// Reads a named `BlendMode` attribute from the input table, outputting each value as an element of a new `Table`. +#[node_macro::node(category("Attributes: Read"))] fn read_attribute_blend_mode( _: impl Ctx, #[implementations( @@ -440,7 +444,7 @@ fn read_attribute_blend_mode( Table, Table, Table, Table, Table, Table, Table, )] content: Table, - /// The attribute name (key) to read from each row. + /// The attribute name (key) to read. name: String, ) -> Table { let mut result = Table::with_capacity(content.len()); @@ -451,8 +455,8 @@ fn read_attribute_blend_mode( result } -/// Reads a named `GradientType` attribute from each row of the input table, outputting a new `Table`. Rows without the attribute are omitted. -#[node_macro::node(category("General"))] +/// Reads a named `GradientType` attribute from the input table, outputting each value as an element of a new `Table`. +#[node_macro::node(category("Attributes: Read"))] fn read_attribute_gradient_type( _: impl Ctx, #[implementations( @@ -460,7 +464,7 @@ fn read_attribute_gradient_type( Table, Table, Table, Table, Table, Table, Table, )] content: Table, - /// The attribute name (key) to read from each row. + /// The attribute name (key) to read. name: String, ) -> Table { let mut result = Table::with_capacity(content.len()); @@ -471,8 +475,8 @@ fn read_attribute_gradient_type( result } -/// Reads a named `GradientSpreadMethod` attribute from each row of the input table, outputting a new `Table`. Rows without the attribute are omitted. -#[node_macro::node(category("General"))] +/// Reads a named `GradientSpreadMethod` attribute from the input table, outputting each value as an element of a new `Table`. +#[node_macro::node(category("Attributes: Read"))] fn read_attribute_spread_method( _: impl Ctx, #[implementations( @@ -480,7 +484,7 @@ fn read_attribute_spread_method( Table, Table, Table, Table, Table, Table, Table, )] content: Table, - /// The attribute name (key) to read from each row. + /// The attribute name (key) to read. name: String, ) -> Table { let mut result = Table::with_capacity(content.len());