From 6ba490bd81ab54b429449c2b538c371deee576e8 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Sat, 2 May 2026 23:19:09 -0700 Subject: [PATCH 1/3] 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 | 16 +- node-graph/libraries/core-types/src/table.rs | 12 +- node-graph/nodes/graphic/src/graphic.rs | 236 +++++++++++++++++- 4 files changed, 308 insertions(+), 11 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 f6f140b40c..9fda547c2c 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]), @@ -140,9 +145,9 @@ fn node_registry() -> HashMap, input: Context, fn_params: [Context => RenderOutput, Context => graphene_std::ContextFeatures]), #[cfg(target_family = "wasm")] async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => CanvasHandle, Context => graphene_std::ContextFeatures]), - // ============= - // MEMOIZE NODES - // ============= + // ========== + // MEMO NODES + // ========== async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => ()]), async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => bool]), async_node!(graphene_core::memo::MemoizeNode<_, _>, 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::MemoizeNode<_, _>, input: Context, fn_params: [Context => Table]), async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Table]), + async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Table]), + async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Table]), + async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Table]), + async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Table]), + async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Table]), #[cfg(target_family = "wasm")] async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => CanvasHandle]), async_node!(graphene_core::memo::MemoizeNode<_, _>, 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 f209d392f0d91dc2e5737c5090f8b25f2174ddd1 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Sun, 3 May 2026 00:11:05 -0700 Subject: [PATCH 2/3] 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()); From 675ce96ec384253a9c8e3da758aad8e41c44d975 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Sun, 3 May 2026 23:55:45 -0700 Subject: [PATCH 3/3] Reduce type explosion --- .../interpreted-executor/src/node_registry.rs | 59 ++++- node-graph/libraries/core-types/src/ops.rs | 31 ++- node-graph/libraries/core-types/src/table.rs | 247 +++++++++++++++++- .../nodes/gcore/src/context_modification.rs | 5 +- node-graph/nodes/graphic/src/graphic.rs | 213 ++++++++------- .../nodes/transform/src/transform_nodes.rs | 17 +- node-graph/nodes/vector/src/vector_nodes.rs | 30 +-- 7 files changed, 446 insertions(+), 156 deletions(-) diff --git a/node-graph/interpreted-executor/src/node_registry.rs b/node-graph/interpreted-executor/src/node_registry.rs index 9fda547c2c..2e66ee7a04 100644 --- a/node-graph/interpreted-executor/src/node_registry.rs +++ b/node-graph/interpreted-executor/src/node_registry.rs @@ -17,7 +17,7 @@ use graphene_std::raster::color::Color; use graphene_std::raster::*; use graphene_std::raster::{CPU, Raster}; use graphene_std::render_node::RenderIntermediate; -use graphene_std::table::Table; +use graphene_std::table::{AttributeColumnDyn, AttributeValueDyn, Table, TableDyn}; use graphene_std::transform::Footprint; use graphene_std::uuid::NodeId; use graphene_std::vector::Vector; @@ -42,6 +42,54 @@ fn node_registry() -> HashMap>, to: Table), #[cfg(feature = "gpu")] convert_node!(from: Table>, to: Table), + // Type-erased attribute column conversions for the `Attach Attribute` node, so it monomorphizes only over the destination table type. + convert_node!(from: Table, to: AttributeColumnDyn), + convert_node!(from: Table, to: AttributeColumnDyn), + convert_node!(from: Table, to: AttributeColumnDyn), + convert_node!(from: Table>, to: AttributeColumnDyn), + convert_node!(from: Table, to: AttributeColumnDyn), + convert_node!(from: Table, to: AttributeColumnDyn), + convert_node!(from: Table, to: AttributeColumnDyn), + convert_node!(from: Table, to: AttributeColumnDyn), + convert_node!(from: Table, to: AttributeColumnDyn), + convert_node!(from: Table, to: AttributeColumnDyn), + convert_node!(from: Table, to: AttributeColumnDyn), + convert_node!(from: Table, to: AttributeColumnDyn), + convert_node!(from: Table, to: AttributeColumnDyn), + convert_node!(from: Table, to: TableDyn), + convert_node!(from: Table, to: TableDyn), + convert_node!(from: Table, to: TableDyn), + convert_node!(from: Table>, to: TableDyn), + #[cfg(feature = "gpu")] + convert_node!(from: Table>, to: TableDyn), + convert_node!(from: Table, to: TableDyn), + convert_node!(from: Table, to: TableDyn), + convert_node!(from: Table, to: TableDyn), + convert_node!(from: Table, to: TableDyn), + convert_node!(from: Table, to: TableDyn), + convert_node!(from: Table, to: TableDyn), + convert_node!(from: Table, to: TableDyn), + convert_node!(from: Table, to: TableDyn), + convert_node!(from: Table, to: TableDyn), + convert_node!(from: Table, to: TableDyn), + convert_node!(from: Table, to: TableDyn), + // Type-erased attribute value conversions for the `Write Attribute` node, so it monomorphizes only over the destination table type. + convert_node!(from: f64, to: AttributeValueDyn), + convert_node!(from: u32, to: AttributeValueDyn), + convert_node!(from: u64, to: AttributeValueDyn), + convert_node!(from: bool, to: AttributeValueDyn), + convert_node!(from: String, to: AttributeValueDyn), + convert_node!(from: DVec2, to: AttributeValueDyn), + convert_node!(from: DAffine2, to: AttributeValueDyn), + convert_node!(from: Color, to: AttributeValueDyn), + convert_node!(from: BlendMode, to: AttributeValueDyn), + convert_node!(from: graphene_std::vector::style::GradientType, to: AttributeValueDyn), + convert_node!(from: graphene_std::vector::style::GradientSpreadMethod, to: AttributeValueDyn), + convert_node!(from: Table, to: AttributeValueDyn), + convert_node!(from: Table, to: AttributeValueDyn), + convert_node!(from: Table, to: AttributeValueDyn), + convert_node!(from: Table, to: AttributeValueDyn), + convert_node!(from: Table, to: AttributeValueDyn), // into_node!(from: Table>, to: Table>), #[cfg(feature = "gpu")] into_node!(from: &PlatformEditorApi, to: &WgpuExecutor), @@ -104,6 +152,9 @@ 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 => AttributeColumnDyn]), + async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => AttributeValueDyn]), + async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => TableDyn]), 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]), @@ -143,6 +194,9 @@ fn node_registry() -> HashMap, input: Context, fn_params: [Context => &PlatformEditorApi, Context => graphene_std::ContextFeatures]), async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => RenderIntermediate, Context => graphene_std::ContextFeatures]), async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => RenderOutput, Context => graphene_std::ContextFeatures]), + async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => AttributeColumnDyn, Context => graphene_std::ContextFeatures]), + async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => AttributeValueDyn, Context => graphene_std::ContextFeatures]), + async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => TableDyn, Context => graphene_std::ContextFeatures]), #[cfg(target_family = "wasm")] async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => CanvasHandle, Context => graphene_std::ContextFeatures]), // ========== @@ -166,6 +220,9 @@ fn node_registry() -> HashMap, input: Context, fn_params: [Context => Table]), async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Table]), async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Table]), + async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => AttributeColumnDyn]), + async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => AttributeValueDyn]), + async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => TableDyn]), #[cfg(target_family = "wasm")] async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => CanvasHandle]), async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => f64]), diff --git a/node-graph/libraries/core-types/src/ops.rs b/node-graph/libraries/core-types/src/ops.rs index 9e0a3e3ffa..9c9205c02a 100644 --- a/node-graph/libraries/core-types/src/ops.rs +++ b/node-graph/libraries/core-types/src/ops.rs @@ -1,7 +1,8 @@ use crate::Node; -use crate::table::{Table, TableRow}; +use crate::table::{AttributeColumnDyn, AttributeValueDyn, Column, Table, TableDyn, TableRow}; use crate::transform::Footprint; use glam::DVec2; +use graphene_hash::CacheHash; use std::future::Future; use std::marker::PhantomData; @@ -71,6 +72,34 @@ impl + Send> Convert, ()> for Table { } } +/// Wraps each row's element into a type-erased column. Lets nodes that accept a source attribute +/// from any `Table` express their signature as `AttributeColumnDyn` and avoid monomorphizing +/// over `U`; the compiler inserts this convert to bridge concrete-typed graph wires to the dyn input. +impl Convert for Table { + async fn convert(self, _: Footprint, _: ()) -> AttributeColumnDyn { + let values: Vec = self.into_iter().map(|row| row.into_element()).collect(); + AttributeColumnDyn(Box::new(Column(values))) + } +} + +/// Wraps a value into a type-erased attribute value. Lets nodes that take a per-item value source +/// (such as `write_attribute`'s value-producing input) be generic over the destination table type +/// alone, with the compiler-inserted convert handling each concrete value type at the wire level. +impl Convert for T { + async fn convert(self, _: Footprint, _: ()) -> AttributeValueDyn { + AttributeValueDyn(Box::new(self)) + } +} + +/// Erases a `Table`'s element type, exposing only its attributes and row count. Lets nodes that +/// only need attribute access (such as the `read_attribute_*` family) take a single `TableDyn` input +/// instead of monomorphizing over every possible carrier table type. +impl Convert for Table { + async fn convert(self, _: Footprint, _: ()) -> TableDyn { + self.into() + } +} + impl Convert for DVec2 { async fn convert(self, _: Footprint, _: ()) -> DVec2 { self diff --git a/node-graph/libraries/core-types/src/table.rs b/node-graph/libraries/core-types/src/table.rs index e4b119c2db..6260ab7d94 100644 --- a/node-graph/libraries/core-types/src/table.rs +++ b/node-graph/libraries/core-types/src/table.rs @@ -83,7 +83,7 @@ pub const ATTR_GRADIENT_TYPE: &str = "gradient_type"; /// Enables type-erased scalar storage that supports Clone, Send, Sync, and downcasting. /// Used for individual attribute values in a TableRow. -trait AttributeValue: std::any::Any + Send + Sync { +pub trait AttributeValue: std::any::Any + Send + Sync { /// Clones this value into a new boxed trait object. fn clone_box(&self) -> Box; @@ -149,7 +149,7 @@ impl Clone for Box { // ====================== /// Enables type-erased columnar storage for parallel attribute lists in a Table. -trait AttributeColumn: std::any::Any + Send + Sync { +pub trait AttributeColumn: std::any::Any + Send + Sync { /// Clones this column into a new boxed trait object. fn clone_box(&self) -> Box; @@ -165,12 +165,21 @@ trait AttributeColumn: std::any::Any + Send + Sync { /// Pushes a default value onto the end of this column. fn push_default(&mut self); + /// Sets the value at the given index, padding with defaults if the column is shorter than `index`. + /// Falls back to a default if the value's type doesn't match. + fn set_at(&mut self, index: usize, value: Box); + /// Creates a new column of the same type filled with `count` number of default values. fn new_with_defaults(&self, count: usize) -> Box; /// Returns the number of elements in this column. fn len(&self) -> usize; + /// Returns whether this column has any elements. + fn is_empty(&self) -> bool { + self.len() == 0 + } + /// Appends all values from another column of the same type. fn extend(&mut self, other: Box); @@ -217,7 +226,7 @@ impl Clone for Box { // ========= /// Wraps a Vec for column-major attribute storage in a Table. -struct Column(Vec); +pub struct Column(pub Vec); impl AttributeColumn for Column { /// Clones this column into a new boxed trait object. @@ -250,6 +259,20 @@ impl self.0.push(T::default()); } + /// Sets the value at the given index, padding with defaults if the column is shorter than `index`. + /// Falls back to a default if the value's type doesn't match. + fn set_at(&mut self, index: usize, value: Box) { + while self.0.len() < index { + self.0.push(T::default()); + } + let value = value.into_any().downcast::().map(|v| *v).unwrap_or_default(); + if self.0.len() == index { + self.0.push(value); + } else { + self.0[index] = value; + } + } + /// Creates a new column filled with `count` default `T` values. fn new_with_defaults(&self, count: usize) -> Box { Box::new(Column(vec![T::default(); count])) @@ -302,6 +325,202 @@ impl } } +// =================== +// AttributeColumnDyn +// =================== + +/// Type-erased column of attribute values, used as a node graph parameter type. +/// Lets a node accept any `Table` source via the auto-inserted `Convert` +/// without monomorphizing over `U` (so the cartesian product of `(content T, source U)` collapses to just `T`). +pub struct AttributeColumnDyn(pub Box); + +impl AttributeColumnDyn { + /// Number of values in this column. + pub fn len(&self) -> usize { + self.0.len() + } + + /// Whether this column has zero values. + pub fn is_empty(&self) -> bool { + self.0.len() == 0 + } + + /// Builds a new column matching `target_len` items, taking values from this column (wrapping if shorter, truncating if longer). + pub fn cloned_to_length(&self, target_len: usize) -> Box { + let mut result = self.0.new_with_defaults(0); + let source_len = self.0.len(); + if source_len == 0 { + for _ in 0..target_len { + result.push_default(); + } + return result; + } + for i in 0..target_len { + let value = self.0.clone_value(i % source_len).expect("source_len > 0"); + result.push(value); + } + result + } +} + +impl Clone for AttributeColumnDyn { + fn clone(&self) -> Self { + Self(self.0.clone_box()) + } +} + +impl Default for AttributeColumnDyn { + fn default() -> Self { + Self(Box::new(Column::(Vec::new()))) + } +} + +impl Debug for AttributeColumnDyn { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "AttributeColumnDyn(len: {})", self.0.len()) + } +} + +impl PartialEq for AttributeColumnDyn { + fn eq(&self, other: &Self) -> bool { + self.0.eq_dyn(&*other.0) + } +} + +impl CacheHash for AttributeColumnDyn { + fn cache_hash(&self, state: &mut H) { + self.0.cache_hash_dyn(state); + } +} + +unsafe impl StaticType for AttributeColumnDyn { + type Static = Self; +} + +// ================== +// AttributeValueDyn +// ================== + +/// Type-erased single attribute value, used as a node graph parameter type. +/// Lets a node accept a value of any concrete type via the auto-inserted `Convert` +/// without monomorphizing over the value type. +pub struct AttributeValueDyn(pub Box); + +impl Clone for AttributeValueDyn { + fn clone(&self) -> Self { + Self(self.0.clone_box()) + } +} + +impl Default for AttributeValueDyn { + fn default() -> Self { + Self(Box::new(false)) + } +} + +impl Debug for AttributeValueDyn { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "AttributeValueDyn({})", self.0.display_string()) + } +} + +impl PartialEq for AttributeValueDyn { + fn eq(&self, other: &Self) -> bool { + self.0.display_string() == other.0.display_string() + } +} + +impl CacheHash for AttributeValueDyn { + fn cache_hash(&self, state: &mut H) { + self.0.display_string().cache_hash(state); + } +} + +unsafe impl StaticType for AttributeValueDyn { + type Static = Self; +} + +// ======== +// TableDyn +// ======== + +/// Type-erased view of a `Table` exposing only its attribute columns and row count, used as a node graph parameter type. +/// Lets a node accept any `Table` source via the auto-inserted `Convert` without monomorphizing over `U`, +/// for cases where the element type is irrelevant (such as nodes that read out a named attribute regardless of the carrier table). +#[derive(Default)] +pub struct TableDyn { + columns: Vec<(String, Box)>, + len: usize, +} + +impl TableDyn { + /// Number of items in the underlying table. + pub fn len(&self) -> usize { + self.len + } + + /// Whether the underlying table has zero items. + pub fn is_empty(&self) -> bool { + self.len == 0 + } + + /// Returns a reference to the attribute value at the given key and item index, downcast to `U`, if present and matching. + pub fn attribute(&self, key: &str, index: usize) -> Option<&U> { + self.columns.iter().find_map(|(k, column)| if k == key { column.get_any(index)?.downcast_ref::() } else { None }) + } +} + +impl From> for TableDyn { + fn from(table: Table) -> Self { + Self { + columns: table.attributes.columns, + len: table.attributes.len, + } + } +} + +impl Clone for TableDyn { + fn clone(&self) -> Self { + Self { + columns: self.columns.iter().map(|(key, column)| (key.clone(), column.clone_box())).collect(), + len: self.len, + } + } +} + +impl Debug for TableDyn { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let keys: Vec<&str> = self.columns.iter().map(|(k, _)| k.as_str()).collect(); + f.debug_struct("TableDyn").field("keys", &keys).field("len", &self.len).finish() + } +} + +impl PartialEq for TableDyn { + fn eq(&self, other: &Self) -> bool { + self.len == other.len + && self.columns.len() == other.columns.len() + && self + .columns + .iter() + .zip(&other.columns) + .all(|((key_a, column_a), (key_b, column_b))| key_a == key_b && column_a.eq_dyn(&**column_b)) + } +} + +impl CacheHash for TableDyn { + fn cache_hash(&self, state: &mut H) { + self.len.cache_hash(state); + for (key, column) in &self.columns { + key.cache_hash(state); + column.cache_hash_dyn(state); + } + } +} + +unsafe impl StaticType for TableDyn { + type Static = Self; +} + // =============== // AttributeValues // =============== @@ -766,6 +985,28 @@ impl Table { self.attributes.set_value(key, index, value); } + /// Replaces (or adds) an attribute column from a type-erased source. The source is wrapped or truncated to match this table's item count. + pub fn set_column_dyn(&mut self, key: impl Into, source: AttributeColumnDyn) { + let key = key.into(); + self.attributes.columns.retain(|(k, _)| k != &key); + let new_column = source.cloned_to_length(self.element.len()); + self.attributes.columns.push((key, new_column)); + } + + /// Sets a single type-erased attribute value at the given index, creating the column from the value's underlying type if it doesn't exist (padded with defaults to match the table's length). Falls back to default if the value's type doesn't match an existing column. + pub fn set_attribute_dyn(&mut self, key: impl Into, index: usize, value: AttributeValueDyn) { + let key = key.into(); + if let Some(position) = self.attributes.columns.iter().position(|(k, _)| k == &key) { + self.attributes.columns[position].1.set_at(index, value.0); + } else { + let mut new_column = value.0.into_column(index); + while new_column.len() < self.element.len() { + new_column.push_default(); + } + self.attributes.columns.push((key, new_column)); + } + } + /// Removes the entire attribute column for the given key, if present. pub fn remove_attribute(&mut self, key: &str) { self.attributes.remove_column(key); diff --git a/node-graph/nodes/gcore/src/context_modification.rs b/node-graph/nodes/gcore/src/context_modification.rs index 627525ea5e..aba8c8d9cd 100644 --- a/node-graph/nodes/gcore/src/context_modification.rs +++ b/node-graph/nodes/gcore/src/context_modification.rs @@ -1,6 +1,6 @@ use core::f64; use core_types::context::{CloneVarArgs, Context, ContextFeatures, Ctx, ExtractAll}; -use core_types::table::Table; +use core_types::table::{AttributeColumnDyn, AttributeValueDyn, Table, TableDyn}; use core_types::transform::Footprint; use core_types::uuid::NodeId; use core_types::{Color, OwnedContextImpl}; @@ -37,6 +37,9 @@ async fn context_modification( Context -> Table, Context -> Table, Context -> Table, + Context -> AttributeColumnDyn, + Context -> AttributeValueDyn, + Context -> TableDyn, )] value: impl Node, Output = T>, /// The parts of the context to keep when evaluating the input value. All other parts are nullified. diff --git a/node-graph/nodes/graphic/src/graphic.rs b/node-graph/nodes/graphic/src/graphic.rs index 4a20645aea..0908aaa700 100644 --- a/node-graph/nodes/graphic/src/graphic.rs +++ b/node-graph/nodes/graphic/src/graphic.rs @@ -1,6 +1,6 @@ use core_types::bounds::{BoundingBox, RenderBoundingBox}; use core_types::registry::types::{Angle, SignedInteger}; -use core_types::table::{Table, TableRow}; +use core_types::table::{AttributeColumnDyn, AttributeValueDyn, Table, TableDyn, TableRow}; use core_types::uuid::NodeId; use core_types::{ATTR_EDITOR_LAYER_PATH, ATTR_TRANSFORM, AnyHash, BlendMode, CacheHash, CloneVarArgs, Color, Context, Ctx, ExtractAll, OwnedContextImpl}; use glam::{DAffine2, DVec2}; @@ -221,104 +221,83 @@ pub fn path_of_subgraph(_: impl Ctx, node_path: Table) -> Table /// 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. +/// The value is type-erased into an `AttributeValueDyn` by an auto-inserted convert node, so this node only +/// monomorphizes over `T` instead of the cartesian product `(T, U)`. #[node_macro::node(category("Attributes: Write"))] -async fn write_attribute( +async fn write_attribute( ctx: impl ExtractAll + CloneVarArgs + Ctx, /// 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, - Table, Table, Table, Table, Table, Table, Table, Table, Table, Table, - Table>, Table>, Table>, Table>, Table>, Table>, Table>, Table>, Table>, Table>, - Table>, Table>, Table>, Table>, Table>, Table>, Table>, Table>, Table>, Table>, - Table, Table, Table, Table, Table, Table, Table, Table, Table, 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 attribute name (key) to write or replace. name: String, /// The node that produces the attribute value for each item. Called once per item with the item's index in context. - #[implementations( - Context -> f64, Context -> u32, Context -> bool, Context -> String, Context -> Table, Context -> DVec2, Context -> DAffine2, Context -> Table, Context -> Table, Context -> Table, - Context -> f64, Context -> u32, Context -> bool, Context -> String, Context -> Table, Context -> DVec2, Context -> DAffine2, Context -> Table, Context -> Table, Context -> Table, - Context -> f64, Context -> u32, Context -> bool, Context -> String, Context -> Table, Context -> DVec2, Context -> DAffine2, Context -> Table, Context -> Table, Context -> Table, - Context -> f64, Context -> u32, Context -> bool, Context -> String, Context -> Table, Context -> DVec2, Context -> DAffine2, Context -> Table, Context -> Table, Context -> Table, - Context -> f64, Context -> u32, Context -> bool, Context -> String, Context -> Table, Context -> DVec2, Context -> DAffine2, Context -> Table, Context -> Table, Context -> Table, - Context -> f64, Context -> u32, Context -> bool, Context -> String, Context -> Table, Context -> DVec2, Context -> DAffine2, Context -> Table, Context -> Table, Context -> Table, - Context -> f64, Context -> u32, Context -> bool, Context -> String, Context -> Table, Context -> DVec2, Context -> DAffine2, Context -> Table, Context -> Table, Context -> Table, - )] - value: impl Node<'n, Context<'static>, Output = U>, + #[implementations(Context -> AttributeValueDyn)] + value: impl Node<'n, Context<'static>, Output = AttributeValueDyn>, ) -> Table { for index in 0..content.len() { let row = content.clone_row(index).expect("index is within bounds"); let owned_ctx = OwnedContextImpl::from(ctx.clone()).with_vararg(Box::new(Table::new_from_row(row))).with_index(index); let v = value.eval(owned_ctx.into_context()).await; - content.set_attribute(&name, index, v); + content.set_attribute_dyn(&name, index, v); } content } /// 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). +/// The source is type-erased into an `AttributeColumnDyn` by an auto-inserted convert node, so this node only monomorphizes over `T` instead of the cartesian product `(T, U)`. #[node_macro::node(category("Attributes: Write"))] -fn attach_attribute( +fn attach_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, + Table, + Table, + Table, + Table>, + Table, + Table, + Table, + Table, + Table, + Table, + Table, + Table, + Table, )] mut content: Table, - /// The table to draw the element values from. + /// The source values to attach. Any `Table` wired here is type-erased via an auto-inserted convert. #[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, + source: AttributeColumnDyn, /// 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.set_column_dyn(name, source); content } /// Reads a named `Vector` attribute from the input table, outputting each value as an element of a new `Table`. #[node_macro::node(category("Attributes: Read"))] -fn read_attribute_vector( +fn read_attribute_vector( _: impl Ctx, - #[implementations( - Table, Table, Table, Table>, Table, Table, - Table, Table, Table, Table, Table, Table, Table, - )] - content: Table, + content: TableDyn, /// The attribute name (key) to read. name: String, ) -> Table { @@ -332,13 +311,9 @@ fn read_attribute_vector( /// 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( +fn read_attribute_number( _: impl Ctx, - #[implementations( - Table, Table, Table, Table>, Table, Table, - Table, Table, Table, Table, Table, Table, Table, - )] - content: Table, + content: TableDyn, /// The attribute name (key) to read. name: String, ) -> Table { @@ -357,13 +332,9 @@ fn read_attribute_number( /// 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( +fn read_attribute_bool( _: impl Ctx, - #[implementations( - Table, Table, Table, Table>, Table, Table, - Table, Table, Table, Table, Table, Table, Table, - )] - content: Table, + content: TableDyn, /// The attribute name (key) to read. name: String, ) -> Table { @@ -377,13 +348,9 @@ fn read_attribute_bool( /// 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( +fn read_attribute_string( _: impl Ctx, - #[implementations( - Table, Table, Table, Table>, Table, Table, - Table, Table, Table, Table, Table, Table, Table, - )] - content: Table, + content: TableDyn, /// The attribute name (key) to read. name: String, ) -> Table { @@ -397,13 +364,9 @@ fn read_attribute_string( /// 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( +fn read_attribute_transform( _: impl Ctx, - #[implementations( - Table, Table, Table, Table>, Table, Table, - Table, Table, Table, Table, Table, Table, Table, - )] - content: Table, + content: TableDyn, /// The attribute name (key) to read. name: String, ) -> Table { @@ -417,13 +380,9 @@ fn read_attribute_transform( /// 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( +fn read_attribute_color( _: impl Ctx, - #[implementations( - Table, Table, Table, Table>, Table, Table, - Table, Table, Table, Table, Table, Table, Table, - )] - content: Table, + content: TableDyn, /// The attribute name (key) to read. name: String, ) -> Table { @@ -437,13 +396,9 @@ fn read_attribute_color( /// 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( +fn read_attribute_blend_mode( _: impl Ctx, - #[implementations( - Table, Table, Table, Table>, Table, Table, - Table, Table, Table, Table, Table, Table, Table, - )] - content: Table, + content: TableDyn, /// The attribute name (key) to read. name: String, ) -> Table { @@ -457,13 +412,9 @@ fn read_attribute_blend_mode( /// 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( +fn read_attribute_gradient_type( _: impl Ctx, - #[implementations( - Table, Table, Table, Table>, Table, Table, - Table, Table, Table, Table, Table, Table, Table, - )] - content: Table, + content: TableDyn, /// The attribute name (key) to read. name: String, ) -> Table { @@ -477,13 +428,9 @@ fn read_attribute_gradient_type( /// 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( +fn read_attribute_spread_method( _: impl Ctx, - #[implementations( - Table, Table, Table, Table>, Table, Table, - Table, Table, Table, Table, Table, Table, Table, - )] - content: Table, + content: TableDyn, /// The attribute name (key) to read. name: String, ) -> Table { @@ -495,6 +442,54 @@ fn read_attribute_spread_method( result } +/// Reads a named `GradientStops` 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_stops( + _: impl Ctx, + content: TableDyn, + /// The attribute name (key) to read. + 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 `Artboard` attribute from the input table, outputting each value as an element of a new `Table`. +#[node_macro::node(category("Attributes: Read"))] +fn read_attribute_artboard( + _: impl Ctx, + content: TableDyn, + /// The attribute name (key) to read. + 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 `Raster` attribute from the input table, outputting each value as an element of a new `Table>`. +#[node_macro::node(category("Attributes: Read"))] +fn read_attribute_raster( + _: impl Ctx, + content: TableDyn, + /// The attribute name (key) to read. + 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 +} + /// 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( diff --git a/node-graph/nodes/transform/src/transform_nodes.rs b/node-graph/nodes/transform/src/transform_nodes.rs index 8776587518..9a7ac72158 100644 --- a/node-graph/nodes/transform/src/transform_nodes.rs +++ b/node-graph/nodes/transform/src/transform_nodes.rs @@ -1,6 +1,6 @@ use core::f64; use core_types::color::Color; -use core_types::table::Table; +use core_types::table::{Table, TableDyn}; use core_types::transform::{ApplyTransform, ScaleType, Transform}; use core_types::{ATTR_TRANSFORM, CloneVarArgs, Context, Ctx, ExtractAll, InjectFootprint, ModifyFootprint, OwnedContextImpl}; use glam::{DAffine2, DMat2, DVec2}; @@ -113,19 +113,8 @@ fn replace_transform( // TODO: Figure out how this node should behave once #2982 is implemented. /// Obtains the transform of the first item in the input `Table`, if present. #[node_macro::node(category("Math: Transform"), path(core_types::vector))] -async fn extract_transform( - _: impl Ctx, - #[implementations( - Table, - Table, - Table>, - Table>, - Table, - Table, - )] - content: Table, -) -> DAffine2 { - content.attribute_cloned_or_default::(ATTR_TRANSFORM, 0) +async fn extract_transform(_: impl Ctx, content: TableDyn) -> DAffine2 { + content.attribute::(ATTR_TRANSFORM, 0).copied().unwrap_or_default() } /// Produces the inverse of the input transform, which is the transform that undoes the effect of the original transform. diff --git a/node-graph/nodes/vector/src/vector_nodes.rs b/node-graph/nodes/vector/src/vector_nodes.rs index f509acf2c8..3aa7db2c2b 100644 --- a/node-graph/nodes/vector/src/vector_nodes.rs +++ b/node-graph/nodes/vector/src/vector_nodes.rs @@ -4,7 +4,7 @@ use core::hash::{Hash, Hasher}; use core_types::blending::BlendMode; use core_types::bounds::{BoundingBox, RenderBoundingBox}; use core_types::registry::types::{Angle, Length, Multiplier, Percentage, PixelLength, Progression, SeedValue}; -use core_types::table::{Table, TableRow}; +use core_types::table::{Table, TableDyn, TableRow}; use core_types::transform::{Footprint, Transform}; use core_types::uuid::NodeId; use core_types::{ @@ -2883,35 +2883,11 @@ fn point_inside(_: impl Ctx, source: Table, point: DVec2) -> bool { }) } -trait Count { - fn count(&self) -> usize; -} -impl Count for Table { - fn count(&self) -> usize { - self.len() - } -} - // TODO: Return u32, u64, or usize instead of f64 after #1621 is resolved and has allowed us to implement automatic type conversion in the node graph for nodes with generic type inputs. // TODO: (Currently automatic type conversion only works for concrete types, via the Graphene preprocessor and not the full Graphene type system.) #[node_macro::node(category("General"), path(graphene_core::vector))] -async fn count_elements( - _: impl Ctx, - #[implementations( - Table, - Table, - Table>, - Table>, - Table, - Table, - Table, - Table, - Table, - Table, - )] - content: I, -) -> f64 { - content.count() as f64 +async fn count_elements(_: impl Ctx, content: TableDyn) -> f64 { + content.len() as f64 } #[node_macro::node(category("Vector: Measure"), path(graphene_core::vector))]