From 585b1dbc34b4ed3f0b6eac409a86d29aa52cbc0e Mon Sep 17 00:00:00 2001 From: Timon Date: Sat, 18 Jul 2026 12:50:56 +0000 Subject: [PATCH 1/3] Add typed attribute keys: attrs! macro, Attr trait, and typed accessors --- node-graph/libraries/core-types/src/attr.rs | 70 ++++++ node-graph/libraries/core-types/src/lib.rs | 2 + node-graph/libraries/core-types/src/list.rs | 219 +++++++++++++++++- .../libraries/graphic-types/src/attr.rs | 31 +++ node-graph/libraries/graphic-types/src/lib.rs | 1 + node-graph/libraries/vector-types/src/attr.rs | 31 +++ node-graph/libraries/vector-types/src/lib.rs | 1 + node-graph/node-macro/src/attrs.rs | 113 +++++++++ node-graph/node-macro/src/lib.rs | 8 + node-graph/nodes/text/src/attr.rs | 23 ++ node-graph/nodes/text/src/lib.rs | 1 + 11 files changed, 496 insertions(+), 4 deletions(-) create mode 100644 node-graph/libraries/core-types/src/attr.rs create mode 100644 node-graph/libraries/graphic-types/src/attr.rs create mode 100644 node-graph/libraries/vector-types/src/attr.rs create mode 100644 node-graph/node-macro/src/attrs.rs create mode 100644 node-graph/nodes/text/src/attr.rs diff --git a/node-graph/libraries/core-types/src/attr.rs b/node-graph/libraries/core-types/src/attr.rs new file mode 100644 index 0000000000..e083c16cc7 --- /dev/null +++ b/node-graph/libraries/core-types/src/attr.rs @@ -0,0 +1,70 @@ +//! Typed attribute keys. +//! +//! Each key is a zero-sized marker implementing [`Attr`], which ties the name (the string +//! stored in the attribute store) to the Rust value type. +//! +//! Keys are declared with the [`node_macro::attrs!`] macro: `Name: Type` entries, where +//! `namespace { ... }` blocks contribute a `namespace:` name prefix. The key name is +//! derived mechanically from the ident (UpperCamel -> snake_case). + +use crate::Color; +use crate::list::NodeIdPath; +use glam::{DAffine2, DVec2}; +use graphene_hash::CacheHash; +use std::fmt::Debug; + +pub trait Attr { + type Value: Clone + Send + Sync + Default + Debug + PartialEq + CacheHash + 'static; + fn name() -> &'static str; +} + +node_macro::attrs! { + /// Item's `DAffine2` transformation, composed multiplicatively through nested groups. + Transform: DAffine2, + /// Item's `BlendMode`, controlling how it composites with content beneath it. + BlendMode: crate::blending::BlendMode, + /// Item's opacity multiplier, composed multiplicatively through nested groups. Affects content clipped to the item. + Opacity: f64, + /// Item's fill opacity multiplier. Like opacity but does not affect content clipped to the item. + OpacityFill: f64, + /// Whether an item inherits the alpha of the content beneath it (clipping mask). + ClippingMask: bool, + /// Byte offset where a regex match begins ('Regex Find All', 'Regex Capture' text nodes). + Start: u64, + /// Byte offset where a regex match ends ('Regex Find All', 'Regex Capture' text nodes). + End: u64, + /// A regex named-capture-group's name, or empty for unnamed groups ('Regex Capture' text node). + Name: String, + /// A JSON value's type (`"string"`, `"number"`, `"object"`, etc.) from 'JSON Query All'. + Type: String, + /// Artboard's top-left corner in document coordinates. + Location: DVec2, + /// Artboard's width and height. + Dimensions: DVec2, + /// Artboard's background fill. + Background: Color, + /// Whether an artboard clips content to its bounds. + Clip: bool, + /// Text item's font size in document-space units. + FontSize: f64, + /// Text item's line height as a ratio of the font size. + LineHeight: f64, + /// Text item's extra spacing between letters in document-space units. + LetterSpacing: f64, + /// Text item's maximum line-wrap width in document-space units. + MaxWidth: Option, + /// Text item's maximum block height in document-space units, past which lines are not drawn. + MaxHeight: Option, + /// Text item's faux-italic letter tilt angle in degrees. + LetterTilt: f64, + editor { + /// Path from the root network to the layer node owning this item. + /// Used by editor tools to route clicks/selection back to the originating layer. + LayerPath: NodeIdPath, + /// Affine mapping the unit square `[(0, 0), (1, 1)]` (top-left convention) onto the 'Text' + /// node's text frame in this item's local space. Each item carries the frame relative to its own + /// glyph origin so it survives `Index Elements` filtering. The Text tool reads this to position + /// its drag cage. Stored as an affine to allow non-axis-aligned frames in the future. + TextFrame: DAffine2, + }, +} diff --git a/node-graph/libraries/core-types/src/lib.rs b/node-graph/libraries/core-types/src/lib.rs index 878e7f5360..32b35976ff 100644 --- a/node-graph/libraries/core-types/src/lib.rs +++ b/node-graph/libraries/core-types/src/lib.rs @@ -1,5 +1,6 @@ extern crate log; +pub mod attr; pub mod bounds; pub mod consts; pub mod context; @@ -16,6 +17,7 @@ pub mod uuid; pub mod value; pub use crate as core_types; +pub use attr::Attr; pub use blending::*; pub use color::Color; pub use context::*; diff --git a/node-graph/libraries/core-types/src/list.rs b/node-graph/libraries/core-types/src/list.rs index 4684721872..b4ca9e255a 100644 --- a/node-graph/libraries/core-types/src/list.rs +++ b/node-graph/libraries/core-types/src/list.rs @@ -1,3 +1,4 @@ +use crate::attr::{self, Attr}; use crate::bounds::{BoundingBox, RenderBoundingBox}; use crate::math::quad::Quad; use crate::transform::ApplyTransform; @@ -132,14 +133,24 @@ unsafe impl StaticType for Bundle { // Implicit attribute defaults // =========================== +// TODO: Remove this is not maintainable /// Overrides the type's default value for certain attributes. fn implicit_default_value(key: &str) -> Option> { - match key { - ATTR_OPACITY | ATTR_OPACITY_FILL => Some(Box::new(1_f64)), - _ => None, + if key == attr::Opacity::name() || key == attr::OpacityFill::name() { + Some(Box::new(1_f64)) + } else { + None } } +/// The value an item without attribute `A` is considered to have: the key's implicit default if it +/// has one, otherwise the value type's `Default`. +fn implicit_default() -> A::Value { + implicit_default_value(A::name()) + .and_then(|value| value.into_any().downcast::().ok()) + .map_or_else(Default::default, |value| *value) +} + /// Appends `count` copies of `key`'s implicit default to `attribute` (see [`implicit_default_value`]). fn pad_with_implicit_default(key: &str, attribute: &mut Box, count: usize) { match implicit_default_value(key) { @@ -500,6 +511,11 @@ impl ListDyn { .iter() .find_map(|(k, attribute)| if k == key { attribute.get_any(index)?.downcast_ref::() } else { None }) } + + /// Returns a reference to the value of the typed attribute at the given item index, if present. + pub fn attr(&self, index: usize) -> Option<&A::Value> { + self.attribute(A::name(), index) + } } impl From> for ListDyn { @@ -622,6 +638,11 @@ impl ItemAttributeValues { /// Gets a mutable reference to the value, inserting a default if it doesn't exist or has the wrong type. pub fn get_or_insert_default_mut(&mut self, key: &str) -> &mut T { + self.get_or_insert_with_mut(key, T::default) + } + + /// Gets a mutable reference to the value, inserting the provided default if it doesn't exist or has the wrong type. + pub fn get_or_insert_with_mut(&mut self, key: &str, default: impl FnOnce() -> T) -> &mut T { let needs_insert = match self.0.iter().position(|(existing_key, _)| existing_key == key) { Some(index) => { if (*self.0[index].1).as_any().downcast_ref::().is_some() { @@ -635,7 +656,7 @@ impl ItemAttributeValues { }; if needs_insert { - self.0.push((key.to_string(), Box::new(T::default()))); + self.0.push((key.to_string(), Box::new(default()))); } self.get_mut::(key).expect("Attribute was just ensured to exist with correct type") @@ -700,6 +721,35 @@ impl ItemAttributeValues { self.0.push((key.to_string(), value)); } } + + // ================== + // Typed key variants + // ================== + + /// Gets a reference to the value of the typed attribute, if present. + pub fn attr(&self) -> Option<&A::Value> { + self.get(A::name()) + } + + /// Gets a mutable reference to the value of the typed attribute, if present. + pub fn attr_mut(&mut self) -> Option<&mut A::Value> { + self.get_mut(A::name()) + } + + /// Gets a mutable reference to the value of the typed attribute, inserting the key's default value if absent. + pub fn attr_mut_or_insert_default(&mut self) -> &mut A::Value { + self.get_or_insert_with_mut(A::name(), implicit_default::) + } + + /// Inserts the typed attribute's value, replacing any existing entry. + pub fn set_attr(&mut self, value: A::Value) { + self.insert(A::name(), value); + } + + /// Removes and returns the value of the typed attribute, if present. + pub fn remove_attr(&mut self) -> Option { + self.remove(A::name()) + } } // ========== @@ -1125,6 +1175,68 @@ impl List { (element.as_mut_slice(), &mut attribute.0) } + // ================== + // Typed key variants + // ================== + + /// Returns a shared reference to the value of the typed attribute at the given item index, if present. + pub fn attr(&self, index: usize) -> Option<&A::Value> { + self.attribute(A::name(), index) + } + + /// Returns a clone of the value of the typed attribute at the given item index, or the key's default value if absent. + pub fn attr_cloned_or_default(&self, index: usize) -> A::Value { + self.attr::(index).cloned().unwrap_or_else(implicit_default::) + } + + /// Returns a clone of the value of the typed attribute at the given item index, or the provided default if absent. + pub fn attr_cloned_or(&self, index: usize, default: A::Value) -> A::Value { + self.attr::(index).cloned().unwrap_or(default) + } + + /// Sets the value of the typed attribute at the given item index, creating the attribute with defaults if it doesn't exist. + pub fn set_attr(&mut self, index: usize, value: A::Value) { + self.set_attribute(A::name(), index, value); + } + + /// Removes the entire typed attribute, if present. + pub fn remove_attr(&mut self) { + self.remove_attribute(A::name()); + } + + /// Runs the given closure on a mutable reference to the value of the typed attribute at the given item index, + /// creating the attribute with defaults if it doesn't exist, and returns the closure's result. + pub fn with_attr_mut_or_default R>(&mut self, index: usize, f: F) -> R { + self.with_attribute_mut_or_default(A::name(), index, f) + } + + /// Returns an iterator over shared references to the values of the typed attribute, or `None` if it doesn't exist. + pub fn iter_attr_values(&self) -> Option> { + self.iter_attribute_values(A::name()) + } + + /// Returns an iterator over mutable references to the values of the typed attribute, or `None` if it doesn't exist. + pub fn iter_attr_values_mut(&mut self) -> Option> { + self.iter_attribute_values_mut(A::name()) + } + + /// Returns an iterator that yields cloned values of the typed attribute, falling back to the key's default value for each item if the attribute is missing. + pub fn iter_attr_values_or_default(&self) -> impl Iterator + '_ { + let slice = self.attributes.get_attribute_slice::(A::name()); + let len = self.element.len(); + (0..len).map(move |i| slice.map_or_else(implicit_default::, |s| s[i].clone())) + } + + /// Returns a mutable iterator over the typed attribute, creating the attribute with defaults if it doesn't exist. + pub fn iter_attr_values_mut_or_default(&mut self) -> std::slice::IterMut<'_, A::Value> { + self.iter_attribute_values_mut_or_default(A::name()) + } + + /// Returns disjoint mutable references to the element slice and the typed attribute's slice, creating the attribute with defaults if it doesn't exist. + pub fn element_and_attr_slices_mut(&mut self) -> (&mut [T], &mut [A::Value]) { + self.element_and_attribute_slices_mut(A::name()) + } + // ================== // Item-level cloning // ================== @@ -1394,6 +1506,56 @@ impl Item { pub fn remove_attribute(&mut self, key: &str) -> Option { self.attributes.remove(key) } + + // ================== + // Typed key variants + // ================== + + /// Returns a reference to the value of the typed attribute, if present. + pub fn attr(&self) -> Option<&A::Value> { + self.attributes.attr::() + } + + /// Returns a reference to the value of the typed attribute, or the provided default if absent. + pub fn attr_or<'a, A: Attr>(&'a self, default: &'a A::Value) -> &'a A::Value { + self.attr::().unwrap_or(default) + } + + /// Returns a clone of the value of the typed attribute, or the provided default if absent. + pub fn attr_cloned_or(&self, default: A::Value) -> A::Value { + self.attr::().cloned().unwrap_or(default) + } + + /// Returns a clone of the value of the typed attribute, or the key's default value if absent. + pub fn attr_cloned_or_default(&self) -> A::Value { + self.attr::().cloned().unwrap_or_else(implicit_default::) + } + + /// Returns a mutable reference to the value of the typed attribute, if present. + pub fn attr_mut(&mut self) -> Option<&mut A::Value> { + self.attributes.attr_mut::() + } + + /// Returns a mutable reference to the value of the typed attribute, inserting the key's default value if absent. + pub fn attr_mut_or_insert_default(&mut self) -> &mut A::Value { + self.attributes.attr_mut_or_insert_default::() + } + + /// Sets the value of the typed attribute, replacing any existing entry. + pub fn set_attr(&mut self, value: A::Value) { + self.attributes.set_attr::(value); + } + + /// Sets the value of the typed attribute and returns the item, enabling builder-style chaining. + pub fn with_attr(mut self, value: A::Value) -> Self { + self.set_attr::(value); + self + } + + /// Removes and returns the value of the typed attribute, if present. + pub fn remove_attr(&mut self) -> Option { + self.attributes.remove_attr::() + } } impl From for Item { @@ -1501,4 +1663,53 @@ mod tests { other.push(Item::new_from_element(()).with_attribute(ATTR_START, 5_u64)); assert_eq!(other.attribute_cloned_or_default::(ATTR_START, 0), 0); } + + // The typed keys must resolve to the same names as the string constants, and the typed + // and string-keyed accessors must hit the same storage. + #[test] + fn typed_attribute_keys() { + use crate::attr; + + assert_eq!(attr::Transform::name(), ATTR_TRANSFORM); + assert_eq!(attr::BlendMode::name(), ATTR_BLEND_MODE); + assert_eq!(attr::Opacity::name(), ATTR_OPACITY); + assert_eq!(attr::OpacityFill::name(), ATTR_OPACITY_FILL); + assert_eq!(attr::ClippingMask::name(), ATTR_CLIPPING_MASK); + assert_eq!(attr::editor::LayerPath::name(), ATTR_EDITOR_LAYER_PATH); + assert_eq!(attr::editor::TextFrame::name(), ATTR_EDITOR_TEXT_FRAME); + assert_eq!(attr::Start::name(), ATTR_START); + assert_eq!(attr::End::name(), ATTR_END); + assert_eq!(attr::Name::name(), ATTR_NAME); + assert_eq!(attr::Type::name(), ATTR_TYPE); + assert_eq!(attr::Location::name(), ATTR_LOCATION); + assert_eq!(attr::Dimensions::name(), ATTR_DIMENSIONS); + assert_eq!(attr::Background::name(), ATTR_BACKGROUND); + assert_eq!(attr::Clip::name(), ATTR_CLIP); + assert_eq!(attr::FontSize::name(), ATTR_FONT_SIZE); + assert_eq!(attr::LineHeight::name(), ATTR_LINE_HEIGHT); + assert_eq!(attr::LetterSpacing::name(), ATTR_LETTER_SPACING); + assert_eq!(attr::MaxWidth::name(), ATTR_MAX_WIDTH); + assert_eq!(attr::MaxHeight::name(), ATTR_MAX_HEIGHT); + assert_eq!(attr::LetterTilt::name(), ATTR_LETTER_TILT); + + // Typed writes are visible through string reads and vice versa + let mut item = Item::new_from_element(()); + item.set_attr::(0.5); + assert_eq!(item.attribute::(ATTR_OPACITY), Some(&0.5)); + item.set_attribute(ATTR_START, 5_u64); + assert_eq!(item.attr::(), Some(&5)); + + // A missing attribute reads as the key's declared default + let empty = Item::new_from_element(()); + assert_eq!(empty.attr_cloned_or_default::(), 1.); + assert_eq!(empty.attr_cloned_or_default::(), 0); + + // The generated implicit-default lookup drives dense-store padding + let mut list = List::<()>::new(); + list.push(Item::new_from_element(())); + list.push(Item::new_from_element(())); + list.set_attr::(1, 0.5); + assert_eq!(list.attr_cloned_or_default::(0), 1.); + assert_eq!(list.attr_cloned_or_default::(1), 0.5); + } } diff --git a/node-graph/libraries/graphic-types/src/attr.rs b/node-graph/libraries/graphic-types/src/attr.rs new file mode 100644 index 0000000000..a34edbb05c --- /dev/null +++ b/node-graph/libraries/graphic-types/src/attr.rs @@ -0,0 +1,31 @@ +//! Typed attribute keys whose value types live in this crate. See `core_types::attr` for the trait and macro. + +use crate::graphic::Graphic; +use core_types::list::List; + +node_macro::attrs! { + /// Vector graphics object's filled area paint. + Fill: List, + /// Vector graphics object's stroke paint. + Stroke: List, + editor { + /// Snapshot of the upstream content that fed into a destructive merge (Boolean Operation, + /// Rasterize, etc.), so the editor can still surface click targets for the original child + /// layers after their content has been collapsed. + MergedLayers: List, + }, +} + +#[cfg(test)] +mod tests { + use super::*; + use core_types::attr::Attr; + + // Key names are the stored document format — pinned as literals so a key rename shows up as a breaking change. + #[test] + fn key_names_are_pinned() { + assert_eq!(Fill::name(), "fill"); + assert_eq!(Stroke::name(), "stroke"); + assert_eq!(editor::MergedLayers::name(), "editor:merged_layers"); + } +} diff --git a/node-graph/libraries/graphic-types/src/lib.rs b/node-graph/libraries/graphic-types/src/lib.rs index d0c940d772..df19bd9492 100644 --- a/node-graph/libraries/graphic-types/src/lib.rs +++ b/node-graph/libraries/graphic-types/src/lib.rs @@ -1,4 +1,5 @@ pub mod artboard; +pub mod attr; pub mod graphic; // Re-export all transitive dependencies so downstream crates only need to depend on graphic-types diff --git a/node-graph/libraries/vector-types/src/attr.rs b/node-graph/libraries/vector-types/src/attr.rs new file mode 100644 index 0000000000..9e12f5e998 --- /dev/null +++ b/node-graph/libraries/vector-types/src/attr.rs @@ -0,0 +1,31 @@ +//! Typed attribute keys whose value types live in this crate. See `core_types::attr` for the trait and macro. + +use crate::gradient::GradientSpreadMethod; +use crate::vector::Vector; + +node_macro::attrs! { + /// Gradient's spread method (`Pad`, `Reflect`, or `Repeat`). + SpreadMethod: GradientSpreadMethod, + /// Gradient's type (`Linear` or `Radial`). + GradientType: crate::gradient::GradientType, + editor { + /// Vector that overrides the item's own geometry for click-target generation. + /// Used by the 'Text' node for per-glyph bounding-box rectangles so glyphs are selectable + /// by clicking anywhere within their bounds, not just the filled letterform. + ClickTarget: Vector, + }, +} + +#[cfg(test)] +mod tests { + use super::*; + use core_types::attr::Attr; + + // Key names are the stored document format — pinned as literals so a key rename shows up as a breaking change. + #[test] + fn key_names_are_pinned() { + assert_eq!(SpreadMethod::name(), "spread_method"); + assert_eq!(GradientType::name(), "gradient_type"); + assert_eq!(editor::ClickTarget::name(), "editor:click_target"); + } +} diff --git a/node-graph/libraries/vector-types/src/lib.rs b/node-graph/libraries/vector-types/src/lib.rs index d66703a690..8da929e954 100644 --- a/node-graph/libraries/vector-types/src/lib.rs +++ b/node-graph/libraries/vector-types/src/lib.rs @@ -1,6 +1,7 @@ #[macro_use] extern crate log; +pub mod attr; pub mod gradient; pub mod math; pub mod subpath; diff --git a/node-graph/node-macro/src/attrs.rs b/node-graph/node-macro/src/attrs.rs new file mode 100644 index 0000000000..5b97e429dd --- /dev/null +++ b/node-graph/node-macro/src/attrs.rs @@ -0,0 +1,113 @@ +use crate::crate_ident::CrateIdent; +use proc_macro2::TokenStream; +use quote::quote; +use syn::parse::ParseStream; +use syn::{Attribute, Ident, Token, Type, braced, token}; + +/// Implementation of the `attrs!` macro declaring typed attribute keys. +/// +/// Grammar: `Name: Type`, comma-separated; `namespace { ... }` blocks nest and contribute a +/// `namespace:` prefix to the key name, which is otherwise derived mechanically from the key +/// ident (UpperCamel → snake_case). +pub fn attrs_impl(input: TokenStream) -> syn::Result { + let entries: Entries = syn::parse2(input)?; + let crate_ident = CrateIdent::default(); + let core = crate_ident.gcore()?; + + let items = entries.0.iter().map(|entry| generate_entry(entry, core, "")).collect::>>()?; + + Ok(quote! { + #(#items)* + }) +} + +struct Entries(Vec); + +enum Entry { + Key { docs: Vec, ident: Ident, ty: Type }, + Namespace { docs: Vec, ident: Ident, entries: Vec }, +} + +impl syn::parse::Parse for Entries { + fn parse(input: ParseStream) -> syn::Result { + Ok(Self(parse_entries(input)?)) + } +} + +fn parse_entries(input: ParseStream) -> syn::Result> { + let mut entries = Vec::new(); + while !input.is_empty() { + let docs = input.call(Attribute::parse_outer)?; + let ident: Ident = input.parse()?; + if input.peek(token::Brace) { + let content; + braced!(content in input); + entries.push(Entry::Namespace { + docs, + ident, + entries: parse_entries(&content)?, + }); + } else { + input.parse::()?; + let ty: Type = input.parse()?; + entries.push(Entry::Key { docs, ident, ty }); + } + if !input.is_empty() { + input.parse::()?; + } + } + Ok(entries) +} + +fn generate_entry(entry: &Entry, core: &TokenStream, prefix: &str) -> syn::Result { + match entry { + Entry::Key { docs, ident, ty } => { + let name = key_name(ident, prefix); + Ok(quote! { + #(#docs)* + pub struct #ident; + impl #core::attr::Attr for #ident { + type Value = #ty; + fn name() -> &'static str { + #name + } + } + }) + } + Entry::Namespace { docs, ident, entries } => { + let child_prefix = child_prefix(ident, prefix); + let items = entries.iter().map(|entry| generate_entry(entry, core, &child_prefix)).collect::>>()?; + Ok(quote! { + #(#docs)* + pub mod #ident { + use super::*; + #(#items)* + } + }) + } + } +} + +fn key_name(ident: &Ident, prefix: &str) -> String { + let snake = snake_case(&ident.to_string()); + if prefix.is_empty() { snake } else { format!("{prefix}:{snake}") } +} + +fn child_prefix(ident: &Ident, prefix: &str) -> String { + if prefix.is_empty() { ident.to_string() } else { format!("{prefix}:{ident}") } +} + +fn snake_case(name: &str) -> String { + let mut result = String::with_capacity(name.len() + 4); + for (i, c) in name.chars().enumerate() { + if c.is_uppercase() { + if i > 0 { + result.push('_'); + } + result.extend(c.to_lowercase()); + } else { + result.push(c); + } + } + result +} diff --git a/node-graph/node-macro/src/lib.rs b/node-graph/node-macro/src/lib.rs index 35fe604a01..33ed7ec5e5 100644 --- a/node-graph/node-macro/src/lib.rs +++ b/node-graph/node-macro/src/lib.rs @@ -3,6 +3,7 @@ use proc_macro::TokenStream; use proc_macro_error2::proc_macro_error; use syn::GenericParam; +mod attrs; mod buffer_struct; mod codegen; mod crate_ident; @@ -33,6 +34,13 @@ pub fn derive_choice_type(input_item: TokenStream) -> TokenStream { derive_choice_type::derive_choice_type_impl(input_item.into()).unwrap_or_else(|err| err.to_compile_error()).into() } +/// Declares typed attribute keys implementing the `Attr` trait, plus a wire-name lookup for their +/// implicit defaults. See `core_types::attr` for the trait and syntax. +#[proc_macro] +pub fn attrs(input: TokenStream) -> TokenStream { + attrs::attrs_impl(input.into()).unwrap_or_else(|err| err.to_compile_error()).into() +} + /// Derive a struct to implement `ShaderStruct`, see that for docs. #[proc_macro_derive(BufferStruct)] pub fn derive_buffer_struct(input_item: TokenStream) -> TokenStream { diff --git a/node-graph/nodes/text/src/attr.rs b/node-graph/nodes/text/src/attr.rs new file mode 100644 index 0000000000..7d8164d58e --- /dev/null +++ b/node-graph/nodes/text/src/attr.rs @@ -0,0 +1,23 @@ +//! Typed attribute keys whose value types live in this crate. See `core_types::attr` for the trait and macro. + +use graphene_resource::Resource; + +node_macro::attrs! { + /// Text item's font, as a resource of the loaded font file. + Font: Resource, + /// Text item's horizontal alignment of lines within the block. + TextAlign: crate::TextAlign, +} + +#[cfg(test)] +mod tests { + use super::*; + use core_types::attr::Attr; + + // Key names are the stored document format — pinned as literals so a key rename shows up as a breaking change. + #[test] + fn key_names_are_pinned() { + assert_eq!(Font::name(), "font"); + assert_eq!(TextAlign::name(), "text_align"); + } +} diff --git a/node-graph/nodes/text/src/lib.rs b/node-graph/nodes/text/src/lib.rs index 6dd4bd6e57..953fec73b2 100644 --- a/node-graph/nodes/text/src/lib.rs +++ b/node-graph/nodes/text/src/lib.rs @@ -1,3 +1,4 @@ +pub mod attr; pub mod fallback; mod font; pub mod json; From 122ad1f5bdf3b21feffb8c70cc61bfb5f68b3397 Mon Sep 17 00:00:00 2001 From: Timon Date: Sat, 18 Jul 2026 13:02:52 +0000 Subject: [PATCH 2/3] Migrate attribute call sites from string keys to typed keys --- .../document/document_message_handler.rs | 2 +- .../node_graph/document_node_definitions.rs | 4 +- .../document/overlays/utility_types_native.rs | 4 +- .../utility_types/document_metadata.rs | 4 +- .../utility_types/network_interface.rs | 4 +- .../tool/tool_messages/artboard_tool.rs | 5 +- editor/src/node_graph_executor.rs | 4 +- editor/src/node_graph_executor/runtime.rs | 5 +- .../src/dynamic_executor/test.rs | 10 +- node-graph/libraries/core-types/src/list.rs | 8 + .../libraries/graphic-types/src/graphic.rs | 125 ++++---- .../libraries/rendering/src/render_ext.rs | 8 +- .../libraries/rendering/src/renderer.rs | 283 +++++++++--------- node-graph/nodes/blending/src/lib.rs | 15 +- node-graph/nodes/brush/src/brush.rs | 16 +- node-graph/nodes/brush/src/brush_cache.rs | 4 +- node-graph/nodes/graphic/src/artboard.rs | 11 +- node-graph/nodes/graphic/src/graphic.rs | 47 +-- .../nodes/gstd/src/platform_application_io.rs | 10 +- node-graph/nodes/gstd/src/text.rs | 18 +- node-graph/nodes/math/src/lib.rs | 4 +- node-graph/nodes/path-bool/src/lib.rs | 82 ++--- node-graph/nodes/raster/src/std_nodes.rs | 22 +- node-graph/nodes/repeat/src/repeat_nodes.rs | 17 +- node-graph/nodes/text/src/json.rs | 4 +- node-graph/nodes/text/src/path_builder.rs | 24 +- node-graph/nodes/text/src/regex.rs | 11 +- node-graph/nodes/text/src/to_path.rs | 46 ++- .../nodes/transform/src/transform_nodes.rs | 9 +- .../vector/src/vector_modification_nodes.rs | 10 +- node-graph/nodes/vector/src/vector_nodes.rs | 224 +++++++------- 31 files changed, 529 insertions(+), 511 deletions(-) diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index a7c57f4659..c55a1da49b 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -2774,7 +2774,7 @@ impl DocumentMessageHandler { let has_fill = fill_graphic_list.is_some_and(|list| is_paint_present(list)); // `Vector.stroke` captures stroke geometry, even with weight 0 or transparent paint. - // So stroke visibility must be checked from `ATTR_STROKE`, the paint source of truth. + // So stroke visibility must be checked from `graphic_types::attr::Stroke`, the paint source of truth. let stroke_visible = stroke_graphic_list.is_some_and(|list| list.element(0).is_some_and(|g| !g.is_fully_transparent())); let has_stroke = stroke.as_ref().is_some_and(|s| s.has_renderable_stroke()) && stroke_visible; diff --git a/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs b/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs index fd7d530d37..39278c5ae7 100644 --- a/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs +++ b/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs @@ -180,7 +180,7 @@ fn document_node_definitions() -> HashMap HashMap(index); let Some(element) = text_list.element(index) else { continue }; for (_, bezier, start_id, end_id) in element.segment_iter() { diff --git a/editor/src/messages/portfolio/document/utility_types/document_metadata.rs b/editor/src/messages/portfolio/document/utility_types/document_metadata.rs index 2676f75dc1..2c3c055757 100644 --- a/editor/src/messages/portfolio/document/utility_types/document_metadata.rs +++ b/editor/src/messages/portfolio/document/utility_types/document_metadata.rs @@ -41,10 +41,10 @@ pub struct DocumentMetadata { /// Vector data keyed by layer ID, used as fallback when no Path node exists. /// This provides accurate SegmentIds for layers without explicit Path nodes. pub layer_vector_data: HashMap>, - /// Per-layer `ATTR_FILL` attribute, exposed so message handlers can read paint + /// Per-layer `graphic_types::attr::Fill` attribute, exposed so message handlers can read paint /// information that lives on the list. pub layer_fill_attributes: HashMap>>, - /// Per-layer `ATTR_STROKE` attribute, exposed so message handlers can read + /// Per-layer `graphic_types::attr::Stroke` attribute, exposed so message handlers can read /// stroke paint information that lives on the list. pub layer_stroke_attributes: HashMap>>, /// Transform from document space to viewport space. diff --git a/editor/src/messages/portfolio/document/utility_types/network_interface.rs b/editor/src/messages/portfolio/document/utility_types/network_interface.rs index b085432dd4..9611f0978b 100644 --- a/editor/src/messages/portfolio/document/utility_types/network_interface.rs +++ b/editor/src/messages/portfolio/document/utility_types/network_interface.rs @@ -3454,12 +3454,12 @@ impl NodeNetworkInterface { self.document_metadata.layer_vector_data = new_layer_vector_data; } - /// Update the per-layer `ATTR_FILL` snapshot. + /// Update the per-layer `graphic_types::attr::Fill` snapshot. pub fn update_fill_attributes(&mut self, new_layer_fill_attributes: HashMap>>) { self.document_metadata.layer_fill_attributes = new_layer_fill_attributes; } - /// Update the per-layer `ATTR_STROKE` snapshot. + /// Update the per-layer `graphic_types::attr::Stroke` snapshot. pub fn update_stroke_attributes(&mut self, new_layer_stroke_attributes: HashMap>>) { self.document_metadata.layer_stroke_attributes = new_layer_stroke_attributes; } diff --git a/editor/src/messages/tool/tool_messages/artboard_tool.rs b/editor/src/messages/tool/tool_messages/artboard_tool.rs index 37cf819c0c..be363d7094 100644 --- a/editor/src/messages/tool/tool_messages/artboard_tool.rs +++ b/editor/src/messages/tool/tool_messages/artboard_tool.rs @@ -572,6 +572,7 @@ impl Fsm for ArtboardToolFsmState { mod test_artboard { pub use crate::test_utils::test_prelude::*; use graphene_std::Artboard; + use graphene_std::attr; use graphene_std::list::List; async fn get_artboards(editor: &mut EditorTestUtils) -> List { @@ -601,8 +602,8 @@ mod test_artboard { let artboards = get_artboards(editor).await; let artboards = (0..artboards.len()) .map(|index| { - let location: DVec2 = artboards.attribute_cloned_or_default(graphene_std::ATTR_LOCATION, index); - let dimensions: DVec2 = artboards.attribute_cloned_or_default(graphene_std::ATTR_DIMENSIONS, index); + let location = artboards.attr_cloned_or_default::(index); + let dimensions = artboards.attr_cloned_or_default::(index); ArtboardLayoutDocument::new(location, dimensions) }) .collect::>(); diff --git a/editor/src/node_graph_executor.rs b/editor/src/node_graph_executor.rs index c58203f714..db3f7531e3 100644 --- a/editor/src/node_graph_executor.rs +++ b/editor/src/node_graph_executor.rs @@ -15,7 +15,7 @@ use graphene_std::raster::{CPU, Raster}; use graphene_std::renderer::{RenderMetadata, graphic_list_bounding_box}; use graphene_std::transform::Footprint; use graphene_std::vector::{Vector, graphic_types}; -use graphene_std::{ATTR_TRANSFORM, Context, Graphic, NodeInputDecleration}; +use graphene_std::{Context, Graphic, NodeInputDecleration, attr}; use interpreted_executor::dynamic_executor::ResolvedDocumentNodeTypesDelta; use std::any::Any; use std::sync::Arc; @@ -870,7 +870,7 @@ fn redirect_export_chain(network: &mut NodeNetwork, full_path: &[NodeId]) -> boo fn measure_fill_geometry(data: &Arc) -> Option<(DAffine2, DAffine2)> { if let Some(list) = introspected_output::>(data) { let vector = list.element(0)?; - let item_transform: DAffine2 = list.attribute_cloned_or_default(ATTR_TRANSFORM, 0); + let item_transform = list.attr_cloned_or_default::(0); let bounds = vector.nonzero_bounding_box(); let bounding_box_affine = DAffine2::from_scale_angle_translation(bounds[1] - bounds[0], 0., bounds[0]); return Some((bounding_box_affine, item_transform)); diff --git a/editor/src/node_graph_executor/runtime.rs b/editor/src/node_graph_executor/runtime.rs index d4be4156de..9b14e4e824 100644 --- a/editor/src/node_graph_executor/runtime.rs +++ b/editor/src/node_graph_executor/runtime.rs @@ -9,6 +9,7 @@ use graph_craft::document::{NodeId, NodeNetwork}; use graph_craft::graphene_compiler::Compiler; use graph_craft::proto::GraphErrors; use graphene_std::application_io::{ApplicationIo, ExportFormat, NodeGraphUpdateMessage, NodeGraphUpdateSender, RenderConfig, Texture}; +use graphene_std::attr; use graphene_std::bounds::RenderBoundingBox; use graphene_std::list::{Item, List}; use graphene_std::memo::IORecord; @@ -536,8 +537,8 @@ impl NodeRuntime { fn artboard_clip_bounds(artboards: &List) -> RenderBoundingBox { let mut combined: Option<[DVec2; 2]> = None; for index in 0..artboards.len() { - let location: DVec2 = artboards.attribute_cloned_or_default(graphene_std::ATTR_LOCATION, index); - let dimensions: DVec2 = artboards.attribute_cloned_or_default(graphene_std::ATTR_DIMENSIONS, index); + let location = artboards.attr_cloned_or_default::(index); + let dimensions = artboards.attr_cloned_or_default::(index); let bounds = [location, location + dimensions]; combined = Some(match combined { Some(existing) => [existing[0].min(bounds[0]), existing[1].max(bounds[1])], diff --git a/node-graph/interpreted-executor/src/dynamic_executor/test.rs b/node-graph/interpreted-executor/src/dynamic_executor/test.rs index 37cba468fb..472f51a8ea 100644 --- a/node-graph/interpreted-executor/src/dynamic_executor/test.rs +++ b/node-graph/interpreted-executor/src/dynamic_executor/test.rs @@ -185,7 +185,7 @@ fn transform_network(content: TaggedValue, rotation: TaggedValue) -> ProtoNetwor #[test] fn transform_composes_onto_item_wire() { - use glam::{DAffine2, DVec2}; + use glam::DVec2; let network = transform_network(TaggedValue::TypeDefault(item!(Vector)), TaggedValue::F64(0.)); let output = network.output; @@ -197,7 +197,7 @@ fn transform_composes_onto_item_wire() { let context: Context = None; let result: Option> = futures::executor::block_on(tree.eval(output, context)); let item = result.expect("A rank-0 chain through Transform should stay rank 0"); - let transform = item.attribute_cloned_or_default::(core_types::ATTR_TRANSFORM); + let transform = item.attr_cloned_or_default::(); assert_eq!(transform.translation, DVec2::new(5., 0.), "The translation should compose onto the item's transform attribute"); } @@ -219,8 +219,8 @@ fn transform_broadcasts_item_content_across_a_framed_parameter() { let list = result.expect("The broadcast should produce a List"); assert_eq!(list.len(), 2, "One output item per frame slot"); - let first: DAffine2 = list.attribute_cloned_or_default(core_types::ATTR_TRANSFORM, 0); - let second: DAffine2 = list.attribute_cloned_or_default(core_types::ATTR_TRANSFORM, 1); + let first: DAffine2 = list.attr_cloned_or_default::(0); + let second: DAffine2 = list.attr_cloned_or_default::(1); assert!((first.matrix2.col(0).y - 0.).abs() < 1e-10, "Slot 0 should be unrotated"); assert!((second.matrix2.col(0).y - 1.).abs() < 1e-10, "Slot 1 should be rotated 90 degrees"); } @@ -320,7 +320,7 @@ fn value_wires_materialize_as_items_at_resolution() { let context: Context = None; let result: Option> = futures::executor::block_on(tree.eval(NodeId(5), context)); let item = result.expect("A value matrix should flow through Transform as an Item"); - let transform = item.attribute_cloned_or_default::(core_types::ATTR_TRANSFORM); + let transform = item.attr_cloned_or_default::(); assert_eq!(transform.translation, DVec2::new(7., 0.), "The translation should compose onto the gained transform attribute"); } diff --git a/node-graph/libraries/core-types/src/list.rs b/node-graph/libraries/core-types/src/list.rs index b4ca9e255a..1247161238 100644 --- a/node-graph/libraries/core-types/src/list.rs +++ b/node-graph/libraries/core-types/src/list.rs @@ -512,6 +512,14 @@ impl ListDyn { .find_map(|(k, attribute)| if k == key { attribute.get_any(index)?.downcast_ref::() } else { None }) } + /// Returns a reference to the attribute value at the given runtime key and item index, downcast to `U`, if present and matching. + /// For keys known at compile time use [`Self::attr`]; this variant is for keys only known at runtime (e.g. the attribute nodes). + pub fn attribute_dyn(&self, key: &str, index: usize) -> Option<&U> { + self.attributes + .iter() + .find_map(|(k, attribute)| if k == key { attribute.get_any(index)?.downcast_ref::() } else { None }) + } + /// Returns a reference to the value of the typed attribute at the given item index, if present. pub fn attr(&self, index: usize) -> Option<&A::Value> { self.attribute(A::name(), index) diff --git a/node-graph/libraries/graphic-types/src/graphic.rs b/node-graph/libraries/graphic-types/src/graphic.rs index e5197f8e55..497ecd7c62 100644 --- a/node-graph/libraries/graphic-types/src/graphic.rs +++ b/node-graph/libraries/graphic-types/src/graphic.rs @@ -1,9 +1,10 @@ +use core_types::Color; +use core_types::attr::{self, Attr}; use core_types::bounds::{BoundingBox, RenderBoundingBox}; use core_types::graphene_hash::CacheHash; -use core_types::list::{ATTR_FILL, ATTR_STROKE, Item, ItemAttributeValues, List, NodeIdPath}; +use core_types::list::{Item, ItemAttributeValues, List}; use core_types::ops::FromAnchorPosition; use core_types::render_complexity::RenderComplexity; -use core_types::{ATTR_CLIPPING_MASK, ATTR_EDITOR_LAYER_PATH, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_TRANSFORM, Color}; use dyn_any::DynAny; use glam::{DAffine2, DVec2}; use raster_types::{CPU, GPU, Raster}; @@ -119,32 +120,32 @@ fn flatten_graphic_list(content: List, extract_variant: fn(Graphic) // Whether the parent carries each attribute: a structural fact (column presence), never a value comparison. // Flattening composes a parent attribute onto its children only when the parent has it, // so an absent parent attribute never invents a column the children didn't already have. - let parent_has_transform = current_graphic_item.attribute::(ATTR_TRANSFORM).is_some(); - let parent_has_opacity = current_graphic_item.attribute::(ATTR_OPACITY).is_some(); - let parent_has_fill = current_graphic_item.attribute::(ATTR_OPACITY_FILL).is_some(); - let parent_has_layer_path = current_graphic_item.attribute::(ATTR_EDITOR_LAYER_PATH).is_some(); + let parent_has_transform = current_graphic_item.attr::().is_some(); + let parent_has_opacity = current_graphic_item.attr::().is_some(); + let parent_has_fill = current_graphic_item.attr::().is_some(); + let parent_has_layer_path = current_graphic_item.attr::().is_some(); - let layer_path: NodeIdPath = current_graphic_item.attribute_cloned_or_default(ATTR_EDITOR_LAYER_PATH); - let current_transform: DAffine2 = current_graphic_item.attribute_cloned_or_default(ATTR_TRANSFORM); - let current_opacity: f64 = current_graphic_item.attribute_cloned_or(ATTR_OPACITY, 1.); - let current_fill: f64 = current_graphic_item.attribute_cloned_or(ATTR_OPACITY_FILL, 1.); + let layer_path = current_graphic_item.attr_cloned_or_default::(); + let current_transform = current_graphic_item.attr_cloned_or_default::(); + let current_opacity = current_graphic_item.attr_cloned_or_default::(); + let current_fill = current_graphic_item.attr_cloned_or_default::(); match current_graphic_item.into_element() { // Compose the parent's transform/opacity/fill onto each child, but only for attributes the parent carries. // A child lacking one is padded with the composition identity (`1.` for opacity/fill, identity for transform), so composing through it is a no-op. Graphic::Graphic(mut sub_list) => { if parent_has_transform { - for v in sub_list.iter_attribute_values_mut_or_default::(ATTR_TRANSFORM) { + for v in sub_list.iter_attr_values_mut_or_default::() { *v = current_transform * *v; } } if parent_has_opacity { - for v in sub_list.iter_attribute_values_mut_or_default::(ATTR_OPACITY) { + for v in sub_list.iter_attr_values_mut_or_default::() { *v *= current_opacity; } } if parent_has_fill { - for v in sub_list.iter_attribute_values_mut_or_default::(ATTR_OPACITY_FILL) { + for v in sub_list.iter_attr_values_mut_or_default::() { *v *= current_fill; } } @@ -155,22 +156,22 @@ fn flatten_graphic_list(content: List, extract_variant: fn(Graphic) other => { if let Some(typed_list) = extract_variant(other) { for mut item in typed_list.into_iter() { - // Each `|| item.attribute(...)` keeps an attribute the item itself carries + // Each `|| item.attr::<...>()` keeps an attribute the item itself carries // (recomposed with the parent's identity value) even when the parent lacks it - if parent_has_transform || item.attribute::(ATTR_TRANSFORM).is_some() { - let item_transform: DAffine2 = item.attribute_cloned_or_default(ATTR_TRANSFORM); - item.set_attribute(ATTR_TRANSFORM, current_transform * item_transform); + if parent_has_transform || item.attr::().is_some() { + let item_transform = item.attr_cloned_or_default::(); + item.set_attr::(current_transform * item_transform); } - if parent_has_opacity || item.attribute::(ATTR_OPACITY).is_some() { - let item_opacity: f64 = item.attribute_cloned_or(ATTR_OPACITY, 1.); - item.set_attribute(ATTR_OPACITY, current_opacity * item_opacity); + if parent_has_opacity || item.attr::().is_some() { + let item_opacity = item.attr_cloned_or_default::(); + item.set_attr::(current_opacity * item_opacity); } - if parent_has_fill || item.attribute::(ATTR_OPACITY_FILL).is_some() { - let item_fill: f64 = item.attribute_cloned_or(ATTR_OPACITY_FILL, 1.); - item.set_attribute(ATTR_OPACITY_FILL, current_fill * item_fill); + if parent_has_fill || item.attr::().is_some() { + let item_fill = item.attr_cloned_or_default::(); + item.set_attr::(current_fill * item_fill); } if parent_has_layer_path { - item.set_attribute(ATTR_EDITOR_LAYER_PATH, layer_path.clone()); + item.set_attr::(layer_path.clone()); } output.push(item); @@ -192,9 +193,9 @@ pub fn is_paint_present(graphic_list: &List) -> bool { graphic_list.element(0).is_some_and(|graphic| !graphic.is_empty()) } -/// Look up the paint graphics stored under attribute for a vector item, in the canonical `List` form. -pub fn graphic_list_at<'a>(list: &'a List, index: usize, attribute: &str) -> Option>> { - list.attribute::>(attribute, index) +/// Look up the paint graphics stored under the typed attribute for a vector item, in the canonical `List` form. +pub fn graphic_list_at>>(list: &List, index: usize) -> Option>> { + list.attr::(index) .map(Cow::Borrowed) // Treat a blank paint attribute as absent so an empty attribute doesn't count as painted .filter(|graphic_list| is_paint_present(graphic_list)) @@ -202,25 +203,25 @@ pub fn graphic_list_at<'a>(list: &'a List, index: usize, attribute: &str /// Whether the item carries a non-blank canonical `List` paint attribute, /// checked by borrowing without cloning the renderable list. -pub fn has_paint_at(list: &List, index: usize, attribute: &str) -> bool { - list.attribute::>(attribute, index).is_some_and(is_paint_present) +pub fn has_paint_at>>(list: &List, index: usize) -> bool { + list.attr::(index).is_some_and(is_paint_present) } /// Stores a paint attribute in its canonical `List` form, the only representation paint readers accept. -pub fn set_paint_attribute(attributes: &mut ItemAttributeValues, key: &str, paint: impl IntoGraphicList) { - attributes.insert(key, paint.into_graphic_list()); +pub fn set_paint_attribute>>(attributes: &mut ItemAttributeValues, paint: impl IntoGraphicList) { + attributes.set_attr::(paint.into_graphic_list()); } /// Stores a paint attribute at a list index in its canonical `List` form, the only representation paint readers accept. -pub fn set_paint_attribute_at(list: &mut List, index: usize, key: &str, paint: impl IntoGraphicList) { - list.set_attribute(key, index, paint.into_graphic_list()); +pub fn set_paint_attribute_at>, T>(list: &mut List, index: usize, paint: impl IntoGraphicList) { + list.set_attr::(index, paint.into_graphic_list()); } /// Bake the provided transform into the per-item transforms of the paint graphics stored under the /// canonical `List` fill and stroke attributes. pub fn bake_paint_transforms(attributes: &mut ItemAttributeValues, transform: DAffine2) { fn bake_list_transform(list: &mut List, transform: DAffine2) { - for item_transform in list.iter_attribute_values_mut_or_default::(ATTR_TRANSFORM) { + for item_transform in list.iter_attr_values_mut_or_default::() { *item_transform = transform * *item_transform; } } @@ -240,7 +241,7 @@ pub fn bake_paint_transforms(attributes: &mut ItemAttributeValues, transform: DA } } - for paint_key in [ATTR_FILL, ATTR_STROKE] { + for paint_key in [crate::attr::Fill::name(), crate::attr::Stroke::name()] { if let Some(graphics) = attributes.get_mut::>(paint_key) { bake_graphic_paint_transform(graphics, transform); } @@ -306,10 +307,10 @@ impl IntoGraphicList for List { fn into_graphic_list(self) -> List { // Propagate the `editor:layer_path` column (if present) from item 0 onto the wrapper Graphic item so a // subsequent `flatten_graphic_list` doesn't drop the inner Vector's layer stamp - let layer_path = self.attribute::(ATTR_EDITOR_LAYER_PATH, 0).cloned(); + let layer_path = self.attr::(0).cloned(); let mut graphic_list = List::new_from_element(Graphic::Vector(self)); if let Some(layer_path) = layer_path { - graphic_list.set_attribute(ATTR_EDITOR_LAYER_PATH, 0, layer_path); + graphic_list.set_attr::(0, layer_path); } graphic_list } @@ -341,10 +342,10 @@ impl IntoGraphicList for List { impl IntoGraphicList for List { fn into_graphic_list(self) -> List { - let layer_path = self.attribute::(ATTR_EDITOR_LAYER_PATH, 0).cloned(); + let layer_path = self.attr::(0).cloned(); let mut graphic_list = List::new_from_element(Graphic::Text(self)); if let Some(layer_path) = layer_path { - graphic_list.set_attribute(ATTR_EDITOR_LAYER_PATH, 0, layer_path); + graphic_list.set_attr::(0, layer_path); } graphic_list } @@ -416,7 +417,7 @@ impl Graphic { pub fn had_clip_enabled(&self) -> bool { fn all_clipped(list: &List) -> bool { - list.iter_attribute_values_or_default::(ATTR_CLIPPING_MASK).all(|clip| clip) + list.iter_attr_values_or_default::().all(|clip| clip) } match self { @@ -435,12 +436,12 @@ impl Graphic { match self { Graphic::Vector(vector) => (0..vector.len()).all(|index| { let Some(element) = vector.element(index) else { return false }; - let opacity: f64 = vector.attribute_cloned_or(ATTR_OPACITY, index, 1.); + let opacity = vector.attr_cloned_or_default::(index); - let fill_opaque_or_absent = graphic_list_at(vector, index, ATTR_FILL).is_none_or(|graphic_list| graphic_list.element(0).is_none_or(|graphic| graphic.is_opaque())); + let fill_opaque_or_absent = graphic_list_at::(vector, index).is_none_or(|graphic_list| graphic_list.element(0).is_none_or(|graphic| graphic.is_opaque())); let stroke_invisible_or_transparent = element.stroke.as_ref().is_none_or(|stroke| !stroke.has_renderable_stroke()) - || graphic_list_at(vector, index, ATTR_STROKE).is_none_or(|graphic_list| graphic_list.element(0).is_none_or(|graphic| graphic.is_fully_transparent())); + || graphic_list_at::(vector, index).is_none_or(|graphic_list| graphic_list.element(0).is_none_or(|graphic| graphic.is_fully_transparent())); opacity > 1. - f64::EPSILON && fill_opaque_or_absent && stroke_invisible_or_transparent }), @@ -453,15 +454,17 @@ impl Graphic { Graphic::None => false, Graphic::Graphic(list) => !list.is_empty() && list.iter_element_values().all(Graphic::is_opaque), Graphic::Vector(list) => { - let is_paint_opaque_at = |key: &str, index: usize| graphic_list_at(list, index, key).is_some_and(|graphic_list| graphic_list.element(0).is_some_and(|graphic| graphic.is_opaque())); + fn is_paint_opaque_at>>(list: &List, index: usize) -> bool { + graphic_list_at::(list, index).is_some_and(|graphic_list| graphic_list.element(0).is_some_and(|graphic| graphic.is_opaque())) + } !list.is_empty() && (0..list.len()).all(|i| { let Some(vector) = list.element(i) else { return false }; - let opacity: f64 = list.attribute_cloned_or(ATTR_OPACITY, i, 1.); - let opacity_fill: f64 = list.attribute_cloned_or(ATTR_OPACITY_FILL, i, 1.); - let fill_opaque = opacity_fill >= 1. - f64::EPSILON && is_paint_opaque_at(ATTR_FILL, i); - let stroke_opaque_or_invisible = vector.stroke.as_ref().is_none_or(|stroke| !stroke.has_renderable_stroke()) || is_paint_opaque_at(ATTR_STROKE, i); + let opacity = list.attr_cloned_or_default::(i); + let opacity_fill = list.attr_cloned_or_default::(i); + let fill_opaque = opacity_fill >= 1. - f64::EPSILON && is_paint_opaque_at::(list, i); + let stroke_opaque_or_invisible = vector.stroke.as_ref().is_none_or(|stroke| !stroke.has_renderable_stroke()) || is_paint_opaque_at::(list, i); opacity >= 1. - f64::EPSILON && fill_opaque && stroke_opaque_or_invisible }) } @@ -477,16 +480,17 @@ impl Graphic { Graphic::Graphic(list) => list.iter_element_values().all(Graphic::is_fully_transparent), Graphic::Vector(list) => (0..list.len()).all(|i| { let Some(vector) = list.element(i) else { return false }; - let is_paint_fully_transparent_at = - |key: &str, index: usize| graphic_list_at(list, index, key).is_none_or(|graphic_list| graphic_list.element(0).is_none_or(|graphic| graphic.is_fully_transparent())); + fn is_paint_fully_transparent_at>>(list: &List, index: usize) -> bool { + graphic_list_at::(list, index).is_none_or(|graphic_list| graphic_list.element(0).is_none_or(|graphic| graphic.is_fully_transparent())) + } - let opacity: f64 = list.attribute_cloned_or(ATTR_OPACITY, i, 1.); + let opacity = list.attr_cloned_or_default::(i); if opacity <= f64::EPSILON { return true; } - let opacity_fill: f64 = list.attribute_cloned_or(ATTR_OPACITY_FILL, i, 1.); - let fill_invisible = opacity_fill <= f64::EPSILON || is_paint_fully_transparent_at(ATTR_FILL, i); - let stroke_invisible = vector.stroke.as_ref().is_none_or(|stroke| !stroke.has_renderable_stroke()) || is_paint_fully_transparent_at(ATTR_STROKE, i); + let opacity_fill = list.attr_cloned_or_default::(i); + let fill_invisible = opacity_fill <= f64::EPSILON || is_paint_fully_transparent_at::(list, i); + let stroke_invisible = vector.stroke.as_ref().is_none_or(|stroke| !stroke.has_renderable_stroke()) || is_paint_fully_transparent_at::(list, i); fill_invisible && stroke_invisible }), Graphic::Color(list) => list.iter_element_values().all(|color| color.a() == 0.), @@ -644,7 +648,7 @@ mod tests { fn flatten_does_not_invent_attributes() { let graphics = List::new_from_element(vector_graphic()); let flattened: List = graphics.into_flattened_list(); - for key in [ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_TRANSFORM, ATTR_EDITOR_LAYER_PATH] { + for key in [attr::Opacity::name(), attr::OpacityFill::name(), attr::Transform::name(), attr::editor::LayerPath::name()] { assert!(!flattened.attribute_keys().any(|k| k == key), "flatten invented the `{key}` attribute"); } } @@ -653,20 +657,19 @@ mod tests { #[test] fn flatten_propagates_present_attributes() { let mut graphics = List::new_from_element(vector_graphic()); - graphics.set_attribute(ATTR_OPACITY, 0, 0.5_f64); + graphics.set_attr::(0, 0.5); let flattened: List = graphics.into_flattened_list(); - assert_eq!(flattened.attribute_cloned_or_default::(ATTR_OPACITY, 0), 0.5); + assert_eq!(flattened.attr_cloned_or_default::(0), 0.5); let mut group = List::new_from_element(Graphic::Graphic(List::new_from_element(vector_graphic()))); - group.set_attribute(ATTR_OPACITY, 0, 0.5_f64); + group.set_attr::(0, 0.5); let flattened: List = group.into_flattened_list(); - assert_eq!(flattened.attribute_cloned_or_default::(ATTR_OPACITY, 0), 0.5); + assert_eq!(flattened.attr_cloned_or_default::(0), 0.5); } } #[cfg(test)] mod graphic_is_opaque_tests { - use core_types::ATTR_SPREAD_METHOD; use vector_types::{GradientSpreadMethod, GradientStop}; use super::*; @@ -678,7 +681,7 @@ mod graphic_is_opaque_tests { fn gradient_graphic(gradient: Gradient) -> Graphic { let mut gradient_list = List::new_from_element(gradient); - gradient_list.set_attribute(ATTR_SPREAD_METHOD, 0, GradientSpreadMethod::Pad); + gradient_list.set_attr::(0, GradientSpreadMethod::Pad); Graphic::Gradient(gradient_list) } diff --git a/node-graph/libraries/rendering/src/render_ext.rs b/node-graph/libraries/rendering/src/render_ext.rs index 1113e5816c..0c845883d0 100644 --- a/node-graph/libraries/rendering/src/render_ext.rs +++ b/node-graph/libraries/rendering/src/render_ext.rs @@ -3,7 +3,7 @@ use crate::{Render, RenderSvgSegmentList, SvgRender}; use core_types::color::SRGBA8; use core_types::list::List; use core_types::uuid::generate_uuid; -use core_types::{ATTR_GRADIENT_TYPE, ATTR_SPREAD_METHOD, ATTR_TRANSFORM, Color}; +use core_types::{Color, attr}; use glam::{DAffine2, DVec2}; use graphic_types::Graphic; use graphic_types::vector_types::gradient::GradientType; @@ -93,9 +93,9 @@ impl RenderExt for List { let mut stop = String::new(); let Some(stops) = self.element(0) else { return 0 }; - let gradient_type: GradientType = self.attribute_cloned_or_default(ATTR_GRADIENT_TYPE, 0); - let local_gradient_transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, 0); - let spread_method: GradientSpreadMethod = self.attribute_cloned_or_default(ATTR_SPREAD_METHOD, 0); + let gradient_type = self.attr_cloned_or_default::(0); + let local_gradient_transform = self.attr_cloned_or_default::(0); + let spread_method = self.attr_cloned_or_default::(0); for (position, color, original_midpoint) in stops.interpolated_samples() { stop.push_str(", multiplied_transform: &DAffine2) -> Option<(peniko::Brush, DAffine2)> { let stops = gradient_list.element(0)?; - let gradient_type: GradientType = gradient_list.attribute_cloned_or_default(ATTR_GRADIENT_TYPE, 0); - let gradient_transform: DAffine2 = gradient_list.attribute_cloned_or_default(ATTR_TRANSFORM, 0); - let spread_method: GradientSpreadMethod = gradient_list.attribute_cloned_or_default(ATTR_SPREAD_METHOD, 0); + let gradient_type = gradient_list.attr_cloned_or_default::(0); + let gradient_transform = gradient_list.attr_cloned_or_default::(0); + let spread_method = gradient_list.attr_cloned_or_default::(0); let mut peniko_stops = peniko::ColorStops::new(); for (position, color, _) in stops.interpolated_samples() { @@ -457,10 +456,10 @@ pub struct RenderMetadata { pub text_frames: HashMap, pub clip_targets: HashSet, pub vector_data: HashMap>, - /// Per-layer `ATTR_FILL` row attribute, exposed so message handlers can read it. + /// Per-layer `graphic_types::attr::Fill` row attribute, exposed so message handlers can read it. #[cfg_attr(feature = "serde", serde(skip))] pub fill_attributes: HashMap>>, - /// Per-layer `ATTR_STROKE` row attribute, exposed so message handlers can read it. + /// Per-layer `graphic_types::attr::Stroke` row attribute, exposed so message handlers can read it. #[cfg_attr(feature = "serde", serde(skip))] pub stroke_attributes: HashMap>>, pub backgrounds: Vec, @@ -581,9 +580,9 @@ impl Render for Graphic { metadata.upstream_footprints.insert(element_id, footprint); // TODO: Find a way to handle more than the first item if !list.is_empty() { - let layer_path: List = list.attribute_cloned_or_default::(ATTR_EDITOR_LAYER_PATH, 0).0; + let layer_path: List = list.attr_cloned_or_default::(0).0; let layer = layer_path.iter_element_values().next_back().copied(); - let transform: DAffine2 = list.attribute_cloned_or_default(ATTR_TRANSFORM, 0); + let transform = list.attr_cloned_or_default::(0); metadata.first_element_source_id.insert(element_id, layer); metadata.local_transforms.insert(element_id, transform); @@ -594,7 +593,7 @@ impl Render for Graphic { // TODO: Find a way to handle more than the first item if !list.is_empty() { - metadata.local_transforms.insert(element_id, list.attribute_cloned_or_default(ATTR_TRANSFORM, 0)); + metadata.local_transforms.insert(element_id, list.attr_cloned_or_default::(0)); } } Graphic::RasterGPU(list) => { @@ -602,7 +601,7 @@ impl Render for Graphic { // TODO: Find a way to handle more than the first item if !list.is_empty() { - metadata.local_transforms.insert(element_id, list.attribute_cloned_or_default(ATTR_TRANSFORM, 0)); + metadata.local_transforms.insert(element_id, list.attr_cloned_or_default::(0)); } } Graphic::Color(list) => { @@ -610,7 +609,7 @@ impl Render for Graphic { // TODO: Find a way to handle more than the first item if !list.is_empty() { - metadata.local_transforms.insert(element_id, list.attribute_cloned_or_default(ATTR_TRANSFORM, 0)); + metadata.local_transforms.insert(element_id, list.attr_cloned_or_default::(0)); } } Graphic::Gradient(list) => { @@ -618,7 +617,7 @@ impl Render for Graphic { // TODO: Find a way to handle more than the first item if !list.is_empty() { - metadata.local_transforms.insert(element_id, list.attribute_cloned_or_default(ATTR_TRANSFORM, 0)); + metadata.local_transforms.insert(element_id, list.attr_cloned_or_default::(0)); } } Graphic::Text(list) => { @@ -626,7 +625,7 @@ impl Render for Graphic { // TODO: Find a way to handle more than the first item if !list.is_empty() { - metadata.local_transforms.insert(element_id, list.attribute_cloned_or_default(ATTR_TRANSFORM, 0)); + metadata.local_transforms.insert(element_id, list.attr_cloned_or_default::(0)); } } } @@ -699,10 +698,10 @@ impl Render for Graphic { /// Reads the artboard metadata for the item at `index` from a `List`. fn read_artboard_attributes(list: &List, index: usize) -> (DVec2, DVec2, Color, bool) { - let location: DVec2 = list.attribute_cloned_or_default(ATTR_LOCATION, index); - let dimensions: DVec2 = list.attribute_cloned_or_default(ATTR_DIMENSIONS, index); - let background: Color = list.attribute_cloned_or_default(ATTR_BACKGROUND, index); - let clip: bool = list.attribute_cloned_or_default(ATTR_CLIP, index); + let location = list.attr_cloned_or_default::(index); + let dimensions = list.attr_cloned_or_default::(index); + let background = list.attr_cloned_or_default::(index); + let clip = list.attr_cloned_or_default::(index); (location, dimensions, background, clip) } @@ -737,7 +736,7 @@ impl Render for List { |attributes| { let matrix = format_transform_matrix(DAffine2::from_translation(location)); if !matrix.is_empty() { - attributes.push(ATTR_TRANSFORM, matrix); + attributes.push("transform", matrix); } if clip { @@ -800,7 +799,7 @@ impl Render for List { let Some(content) = self.element(index).map(Artboard::as_graphic_list) else { continue }; let (location, dimensions, _background, clip) = read_artboard_attributes(self, index); - let layer_path: List = self.attribute_cloned_or_default::(ATTR_EDITOR_LAYER_PATH, index).0; + let layer_path: List = self.attr_cloned_or_default::(index).0; let element_id = layer_path.iter_element_values().next_back().copied(); if let Some(element_id) = element_id { @@ -823,7 +822,7 @@ impl Render for List { fn add_upstream_click_targets(&self, click_targets: &mut Vec) { for index in 0..self.len() { - let dimensions: DVec2 = self.attribute_cloned_or_default(ATTR_DIMENSIONS, index); + let dimensions = self.attr_cloned_or_default::(index); let subpath_rectangle = Subpath::new_rectangle(DVec2::ZERO, dimensions); click_targets.push(ClickTarget::new_with_subpath(subpath_rectangle, 0.)); } @@ -839,10 +838,10 @@ impl Render for List { let mut mask_state = None; for index in 0..self.len() { - let transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index); - let blend_mode: BlendMode = self.attribute_cloned_or_default(ATTR_BLEND_MODE, index); - let opacity_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY, index, 1.); - let opacity_fill_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY_FILL, index, 1.); + let transform = self.attr_cloned_or_default::(index); + let blend_mode = self.attr_cloned_or_default::(index); + let opacity_attr = self.attr_cloned_or_default::(index); + let opacity_fill_attr = self.attr_cloned_or_default::(index); let element = self.element(index).unwrap(); render.parent_tag( @@ -850,7 +849,7 @@ impl Render for List { |attributes| { let matrix = format_transform_matrix(transform); if !matrix.is_empty() { - attributes.push(ATTR_TRANSFORM, matrix); + attributes.push("transform", matrix); } let opacity = (opacity_attr * if render_params.for_mask { 1. } else { opacity_fill_attr }) as f32; @@ -895,11 +894,11 @@ impl Render for List { let mut mask_element_and_transform = None; for index in 0..self.len() { - let item_transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index); + let item_transform = self.attr_cloned_or_default::(index); let transform = transform * item_transform; - let blend_mode_attr: BlendMode = self.attribute_cloned_or_default(ATTR_BLEND_MODE, index); - let opacity_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY, index, 1.); - let opacity_fill_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY_FILL, index, 1.); + let blend_mode_attr = self.attr_cloned_or_default::(index); + let opacity_attr = self.attr_cloned_or_default::(index); + let opacity_fill_attr = self.attr_cloned_or_default::(index); let element = self.element(index).unwrap(); let mut layer = false; @@ -971,8 +970,8 @@ impl Render for List { fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, element_id: Option) { for index in 0..self.len() { - let item_transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index); - let layer_path: List = self.attribute_cloned_or_default::(ATTR_EDITOR_LAYER_PATH, index).0; + let item_transform = self.attr_cloned_or_default::(index); + let layer_path: List = self.attr_cloned_or_default::(index).0; let layer = layer_path.iter_element_values().next_back().copied(); let element = self.element(index).unwrap(); @@ -992,7 +991,7 @@ impl Render for List { let mut all_upstream_outlines = Vec::new(); for index in 0..self.len() { - let item_transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index); + let item_transform = self.attr_cloned_or_default::(index); let element = self.element(index).unwrap(); let mut new_click_targets = Vec::new(); @@ -1019,7 +1018,7 @@ impl Render for List { fn add_upstream_click_targets(&self, click_targets: &mut Vec) { for index in 0..self.len() { - let item_transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index); + let item_transform = self.attr_cloned_or_default::(index); let element = self.element(index).unwrap(); let mut new_click_targets = Vec::new(); @@ -1035,7 +1034,7 @@ impl Render for List { fn add_upstream_outline_targets(&self, outlines: &mut Vec) { for index in 0..self.len() { - let item_transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index); + let item_transform = self.attr_cloned_or_default::(index); let element = self.element(index).unwrap(); let mut new_outlines = Vec::new(); @@ -1054,7 +1053,7 @@ impl Render for List { } fn new_ids_from_hash(&mut self, _reference: Option) { - let (elements, layers) = self.element_and_attribute_slices_mut::(ATTR_EDITOR_LAYER_PATH); + let (elements, layers) = self.element_and_attr_slices_mut::(); for (element, layer) in elements.iter_mut().zip(layers.iter()) { element.new_ids_from_hash(layer.0.iter_element_values().next_back().copied()); } @@ -1065,10 +1064,10 @@ impl Render for List { fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) { for index in 0..self.len() { let Some(vector) = self.element(index) else { continue }; - let item_transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index); - let blend_mode_attr: BlendMode = self.attribute_cloned_or_default(ATTR_BLEND_MODE, index); - let opacity_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY, index, 1.); - let opacity_fill_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY_FILL, index, 1.); + let item_transform = self.attr_cloned_or_default::(index); + let blend_mode_attr = self.attr_cloned_or_default::(index); + let opacity_attr = self.attr_cloned_or_default::(index); + let opacity_fill_attr = self.attr_cloned_or_default::(index); // Only consider strokes with non-zero weight, since default strokes with zero weight would prevent assigning the correct stroke transform let has_real_stroke = vector.stroke.as_ref().filter(|stroke| stroke.weight() > 0.); @@ -1097,10 +1096,10 @@ impl Render for List { MaskType::Mask }; - let fill_graphic_list = graphic_list_at(self, index, ATTR_FILL); + let fill_graphic_list = graphic_list_at::(self, index); let fill_graphic = fill_graphic_list.as_ref().and_then(|l| l.element(0)); - let stroke_graphic_list = graphic_list_at(self, index, ATTR_STROKE); + let stroke_graphic_list = graphic_list_at::(self, index); let stroke_graphic = stroke_graphic_list.as_ref().and_then(|l| l.element(0)); let path_is_closed = vector.stroke_bezier_paths().all(|path| path.closed()); @@ -1135,8 +1134,8 @@ impl Render for List { // The mask must draw at full alpha so the SVG ``/`` fully zeroes the path interior. // The wrapping SVG group (above) handles the user-set opacity. - let mut mask_item = Item::new_from_element(cloned_vector).with_attribute(ATTR_TRANSFORM, item_transform); - set_paint_attribute(mask_item.attributes_mut(), ATTR_FILL, List::new_from_element(Color::BLACK)); + let mut mask_item = Item::new_from_element(cloned_vector).with_attr::(item_transform); + set_paint_attribute::(mask_item.attributes_mut(), List::new_from_element(Color::BLACK)); let vector_item = List::new_from_item(mask_item); (id, mask_type, vector_item) @@ -1164,7 +1163,7 @@ impl Render for List { attributes.push("d", path.clone()); let matrix = format_transform_matrix(element_transform); if !matrix.is_empty() { - attributes.push(ATTR_TRANSFORM, matrix); + attributes.push("transform", matrix); } let defs = &mut attributes.0.svg_defs; @@ -1281,10 +1280,10 @@ impl Render for List { use graphic_types::vector_types::vector; let Some(element) = self.element(index) else { continue }; - let item_transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index); - let blend_mode_attr: BlendMode = self.attribute_cloned_or_default(ATTR_BLEND_MODE, index); - let opacity_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY, index, 1.); - let opacity_fill_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY_FILL, index, 1.); + let item_transform = self.attr_cloned_or_default::(index); + let blend_mode_attr = self.attr_cloned_or_default::(index); + let opacity_attr = self.attr_cloned_or_default::(index); + let opacity_fill_attr = self.attr_cloned_or_default::(index); let multiplied_transform = parent_transform * item_transform; let has_real_stroke = element.stroke.as_ref().filter(|stroke| stroke.weight() > 0.); let set_stroke_transform = has_real_stroke.map(|stroke| stroke.transform).filter(|transform| transform_is_invertible(*transform)); @@ -1310,8 +1309,8 @@ impl Render for List { } } - let fill_graphic_list = graphic_list_at(self, index, ATTR_FILL); - let stroke_graphic_list = graphic_list_at(self, index, ATTR_STROKE); + let fill_graphic_list = graphic_list_at::(self, index); + let stroke_graphic_list = graphic_list_at::(self, index); // If we're using opacity or a blend mode, we need to push a layer let blend_mode = match render_params.render_mode { @@ -1484,8 +1483,8 @@ impl Render for List { // The mask must draw at full alpha so `SrcOut` fully zeroes the path interior. // The outer opacity/blend layer (above) handles the user-set opacity. - let mut mask_item = Item::new_from_element(cloned_element).with_attribute(ATTR_TRANSFORM, item_transform); - set_paint_attribute(mask_item.attributes_mut(), ATTR_FILL, List::new_from_element(Color::BLACK)); + let mut mask_item = Item::new_from_element(cloned_element).with_attr::(item_transform); + set_paint_attribute::(mask_item.attributes_mut(), List::new_from_element(Color::BLACK)); let vector_list = List::new_from_item(mask_item); let bounds = element.bounding_box_with_transform(multiplied_transform).unwrap_or(layer_bounds); @@ -1559,7 +1558,7 @@ impl Render for List { // Aggregate all items' targets per element_id so multi-item lists (e.g. the "Text to Vector Glyphs" node) produce hit areas for every glyph. // Targets are baked relative to item 0's transform since `Graphic::collect_metadata` records that as `local_transforms[element_id]`. let item_zero_transform: DAffine2 = if !self.is_empty() { - self.attribute_cloned_or_default(ATTR_TRANSFORM, 0) + self.attr_cloned_or_default::(0) } else { DAffine2::IDENTITY }; @@ -1574,8 +1573,8 @@ impl Render for List { for index in 0..self.len() { let Some(source) = self.element(index) else { continue }; - let transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index); - let layer_path: List = self.attribute_cloned_or_default::(ATTR_EDITOR_LAYER_PATH, index).0; + let transform = self.attr_cloned_or_default::(index); + let layer_path: List = self.attr_cloned_or_default::(index).0; let layer = layer_path.iter_element_values().next_back().copied(); if let Some(element_id) = caller_element_id.or(layer) { @@ -1588,7 +1587,7 @@ impl Render for List { } // Use click-target override if the item provides one (e.g. 'Text' node's per-glyph bboxes) - let click_target_vector = self.attribute::(ATTR_EDITOR_CLICK_TARGET, index).unwrap_or(source); + let click_target_vector = self.attr::(index).unwrap_or(source); let item_relative_transform = item_zero_inverse * transform; @@ -1608,16 +1607,16 @@ impl Render for List { if let std::collections::hash_map::Entry::Vacant(e) = metadata.vector_data.entry(element_id) { e.insert(Arc::new(source.clone())); - if let Some(fill_graphic) = graphic_list_at(self, index, ATTR_FILL) { + if let Some(fill_graphic) = graphic_list_at::(self, index) { metadata.fill_attributes.insert(element_id, Arc::new(fill_graphic.into_owned())); } - if let Some(stroke_graphic) = graphic_list_at(self, index, ATTR_STROKE) { + if let Some(stroke_graphic) = graphic_list_at::(self, index) { metadata.stroke_attributes.insert(element_id, Arc::new(stroke_graphic.into_owned())); } } // Surface `editor:text_frame` for the Text tool's drag cage - if let Some(&frame) = self.attribute::(ATTR_EDITOR_TEXT_FRAME, index) { + if let Some(&frame) = self.attr::(index) { metadata.text_frames.entry(element_id).or_insert(frame); } } @@ -1625,7 +1624,7 @@ impl Render for List { // If this item carries a snapshot of upstream graphic content (e.g. it was produced by Boolean Operation, // Combine Paths, Morph, or any other destructive merge), recurse into that snapshot so the editor can // surface the original child layers' click targets. - let upstream_nested_layers = self.attribute_cloned_or_default::>(ATTR_EDITOR_MERGED_LAYERS, index); + let upstream_nested_layers = self.attr_cloned_or_default::(index); if !upstream_nested_layers.is_empty() { let mut upstream_footprint = footprint; upstream_footprint.transform *= transform; @@ -1645,10 +1644,10 @@ impl Render for List { fn add_upstream_click_targets(&self, click_targets: &mut Vec) { for index in 0..self.len() { let Some(source) = self.element(index) else { continue }; - let transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index); + let transform = self.attr_cloned_or_default::(index); // Use click-target override geometry if the item provides one (e.g. 'Text' node's per-glyph bounding boxes) - let vector = self.attribute::(ATTR_EDITOR_CLICK_TARGET, index).unwrap_or(source); + let vector = self.attr::(index).unwrap_or(source); extend_targets_from_vector(click_targets, self, index, vector, transform); } @@ -1658,7 +1657,7 @@ impl Render for List { // Source geometry only, ignoring `editor:click_target`, so outlines reflect actual letterforms for index in 0..self.len() { let Some(source) = self.element(index) else { continue }; - let transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index); + let transform = self.attr_cloned_or_default::(index); extend_targets_from_vector(outlines, self, index, source, transform); } @@ -1674,7 +1673,7 @@ impl Render for List { /// Build one `CompoundPath` (non-zero fill rule, so holes like the inside of an "O" work /// correctly) plus one `FreePoint` per disconnected anchor, apply the transform, and append. fn extend_targets_from_vector(targets: &mut Vec, vector_list: &List, index: usize, geometry: &Vector, transform: DAffine2) { - let filled = has_paint_at(vector_list, index, ATTR_FILL); + let filled = has_paint_at::(vector_list, index); let mut subpaths: Vec> = geometry.stroke_bezier_paths().collect(); let all_subpaths_closed = subpaths.iter().all(|subpath| subpath.closed()); @@ -1730,10 +1729,10 @@ impl Render for List> { for index in 0..self.len() { let Some(image) = self.element(index) else { continue }; - let transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index); - let blend_mode_attr: BlendMode = self.attribute_cloned_or_default(ATTR_BLEND_MODE, index); - let opacity_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY, index, 1.); - let opacity_fill_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY_FILL, index, 1.); + let transform = self.attr_cloned_or_default::(index); + let blend_mode_attr = self.attr_cloned_or_default::(index); + let opacity_attr = self.attr_cloned_or_default::(index); + let opacity_fill_attr = self.attr_cloned_or_default::(index); if image.data.is_empty() { continue; @@ -1752,7 +1751,7 @@ impl Render for List> { let matrix = transform * DAffine2::from_scale(1. / size); let matrix = format_transform_matrix(matrix); if !matrix.is_empty() { - attributes.push(ATTR_TRANSFORM, matrix); + attributes.push("transform", matrix); } attributes.push("width", size.x.to_string()); @@ -1795,7 +1794,7 @@ impl Render for List> { attributes.push("href", base64_string); let matrix = format_transform_matrix(transform); if !matrix.is_empty() { - attributes.push(ATTR_TRANSFORM, matrix); + attributes.push("transform", matrix); } let opacity = (opacity_attr * if render_params.for_mask { 1. } else { opacity_fill_attr }) as f32; @@ -1817,9 +1816,9 @@ impl Render for List> { continue; } - let blend_mode_attr: BlendMode = self.attribute_cloned_or_default(ATTR_BLEND_MODE, index); - let opacity_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY, index, 1.); - let opacity_fill_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY_FILL, index, 1.); + let blend_mode_attr = self.attr_cloned_or_default::(index); + let opacity_attr = self.attr_cloned_or_default::(index); + let opacity_fill_attr = self.attr_cloned_or_default::(index); let blend_mode = blend_mode_attr.to_peniko(); let opacity = (opacity_attr * if render_params.for_mask { 1. } else { opacity_fill_attr }) as f32; @@ -1834,7 +1833,7 @@ impl Render for List> { layer = true; } - let transform_attribute: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index); + let transform_attribute = self.attr_cloned_or_default::(index); if let RenderMode::Outline = render_params.render_mode { let outline_transform: DAffine2 = transform * transform_attribute; @@ -1874,7 +1873,7 @@ impl Render for List> { metadata.upstream_footprints.insert(element_id, footprint); // TODO: Find a way to handle more than one item of the `List>` if !self.is_empty() { - let transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, 0); + let transform = self.attr_cloned_or_default::(0); metadata.local_transforms.insert(element_id, transform); // If this raster carries a snapshot of upstream graphic content (e.g. it was produced by Rasterize, @@ -1883,7 +1882,7 @@ impl Render for List> { // The snapshot was captured before Rasterize shifted its input transforms to align with the rasterization // area, so the children are already in the coordinate space matching `footprint` here — we must NOT // multiply in `transform` (which is the rasterization area, not a layer-stack transform). - let upstream_nested_layers = self.attribute_cloned_or_default::>(ATTR_EDITOR_MERGED_LAYERS, 0); + let upstream_nested_layers = self.attr_cloned_or_default::(0); if !upstream_nested_layers.is_empty() { upstream_nested_layers.collect_metadata(metadata, footprint, None); } @@ -1906,10 +1905,10 @@ impl Render for List> { fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2, context: &mut RenderContext, render_params: &RenderParams) { for index in 0..self.len() { let Some(raster) = self.element(index) else { continue }; - let blend_mode_attr: BlendMode = self.attribute_cloned_or_default(ATTR_BLEND_MODE, index); - let opacity_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY, index, 1.); - let opacity_fill_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY_FILL, index, 1.); - let clip_attr: bool = self.attribute_cloned_or_default(ATTR_CLIPPING_MASK, index); + let blend_mode_attr = self.attr_cloned_or_default::(index); + let opacity_attr = self.attr_cloned_or_default::(index); + let opacity_fill_attr = self.attr_cloned_or_default::(index); + let clip_attr = self.attr_cloned_or_default::(index); let blend_mode = match render_params.render_mode { RenderMode::Outline => peniko::Mix::Normal, _ => blend_mode_attr.to_peniko(), @@ -1928,7 +1927,7 @@ impl Render for List> { layer = true; } - let transform_attribute: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index); + let transform_attribute = self.attr_cloned_or_default::(index); if let RenderMode::Outline = render_params.render_mode { let outline_transform = transform * transform_attribute; @@ -1969,7 +1968,7 @@ impl Render for List> { metadata.upstream_footprints.insert(element_id, footprint); // TODO: Find a way to handle more than one item of the `List>` if !self.is_empty() { - let transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, 0); + let transform = self.attr_cloned_or_default::(0); metadata.local_transforms.insert(element_id, transform); // If this raster carries a snapshot of upstream graphic content (e.g. it was produced by Rasterize, @@ -1978,7 +1977,7 @@ impl Render for List> { // The snapshot was captured before Rasterize shifted its input transforms to align with the rasterization // area, so the children are already in the coordinate space matching `footprint` here — we must NOT // multiply in `transform` (which is the rasterization area, not a layer-stack transform). - let upstream_nested_layers = self.attribute_cloned_or_default::>(ATTR_EDITOR_MERGED_LAYERS, 0); + let upstream_nested_layers = self.attr_cloned_or_default::(0); if !upstream_nested_layers.is_empty() { upstream_nested_layers.collect_metadata(metadata, footprint, None); } @@ -2000,9 +1999,9 @@ impl Render for List> { impl Render for List { fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) { for (index, color) in self.iter_element_values().enumerate() { - let blend_mode: BlendMode = self.attribute_cloned_or_default(ATTR_BLEND_MODE, index); - let opacity_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY, index, 1.); - let opacity_fill_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY_FILL, index, 1.); + let blend_mode = self.attr_cloned_or_default::(index); + let opacity_attr = self.attr_cloned_or_default::(index); + let opacity_fill_attr = self.attr_cloned_or_default::(index); render.leaf_tag("polyline", |attributes| { // Stand-in for an infinite background. Chrome's SVG renderer keeps internal coordinates in f32 and loses // precision past ~2^24 (~16.7 million), causing tile-boundary artifacts that pop in and out during panning. @@ -2031,9 +2030,9 @@ impl Render for List { use vello::peniko; for (index, color) in self.iter_element_values().enumerate() { - let blend_mode_attr: BlendMode = self.attribute_cloned_or_default(ATTR_BLEND_MODE, index); - let opacity_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY, index, 1.); - let opacity_fill_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY_FILL, index, 1.); + let blend_mode_attr = self.attr_cloned_or_default::(index); + let opacity_attr = self.attr_cloned_or_default::(index); + let opacity_fill_attr = self.attr_cloned_or_default::(index); let blend_mode = blend_mode_attr.to_peniko(); let opacity = (opacity_attr * if render_params.for_mask { 1. } else { opacity_fill_attr }) as f32; @@ -2071,12 +2070,12 @@ impl Render for List { for index in 0..self.len() { let Some(gradient) = self.element(index) else { continue }; - let transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index); - let blend_mode: BlendMode = self.attribute_cloned_or_default(ATTR_BLEND_MODE, index); - let opacity_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY, index, 1.); - let opacity_fill_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY_FILL, index, 1.); - let spread_method: GradientSpreadMethod = self.attribute_cloned_or_default(ATTR_SPREAD_METHOD, index); - let gradient_type: GradientType = self.attribute_cloned_or_default(ATTR_GRADIENT_TYPE, index); + let transform = self.attr_cloned_or_default::(index); + let blend_mode = self.attr_cloned_or_default::(index); + let opacity_attr = self.attr_cloned_or_default::(index); + let opacity_fill_attr = self.attr_cloned_or_default::(index); + let spread_method = self.attr_cloned_or_default::(index); + let gradient_type = self.attr_cloned_or_default::(index); let tag = if thumbnail_rect.is_some() { "rect" } else { "polyline" }; render.leaf_tag(tag, |attributes| { if let Some((min, size)) = thumbnail_rect { @@ -2160,13 +2159,13 @@ impl Render for List { for (((index, gradient), spread_method), gradient_type) in self .iter_element_values() .enumerate() - .zip(self.iter_attribute_values_or_default::(ATTR_SPREAD_METHOD)) - .zip(self.iter_attribute_values_or_default::(ATTR_GRADIENT_TYPE)) + .zip(self.iter_attr_values_or_default::()) + .zip(self.iter_attr_values_or_default::()) { - let transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index); - let blend_mode_attr: BlendMode = self.attribute_cloned_or_default(ATTR_BLEND_MODE, index); - let opacity_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY, index, 1.); - let opacity_fill_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY_FILL, index, 1.); + let transform = self.attr_cloned_or_default::(index); + let blend_mode_attr = self.attr_cloned_or_default::(index); + let opacity_attr = self.attr_cloned_or_default::(index); + let opacity_fill_attr = self.attr_cloned_or_default::(index); let gradient_transform = parent_transform * transform; let blend_mode = blend_mode_attr.to_peniko(); @@ -2314,16 +2313,16 @@ fn draw_glyph_run_to_bezpaths(glyph_run: &parley::GlyphRun<'_, ()>, x_offset: f3 fn text_item_size_and_transform(list: &List, index: usize) -> Option<(DVec2, DAffine2)> { let text = list.element(index)?; let font: Resource = { - let f: Resource = list.attribute_cloned_or_default(ATTR_FONT, index); + let f = list.attr_cloned_or_default::(index); if f.is_empty() { text_nodes::FALLBACK_FONT_RESOURCE.clone() } else { f } }; - let font_size: f64 = list.attribute_cloned_or(ATTR_FONT_SIZE, index, DEFAULT_FONT_SIZE); - let line_height: f64 = list.attribute_cloned_or(ATTR_LINE_HEIGHT, index, 1.2); - let letter_spacing: f64 = list.attribute_cloned_or(ATTR_LETTER_SPACING, index, 0.); - let max_width: Option = list.attribute_cloned_or(ATTR_MAX_WIDTH, index, None); - let max_height: Option = list.attribute_cloned_or(ATTR_MAX_HEIGHT, index, None); - let align: text_nodes::TextAlign = list.attribute_cloned_or_default(ATTR_TEXT_ALIGN, index); - let transform: DAffine2 = list.attribute_cloned_or_default(ATTR_TRANSFORM, index); + let font_size = list.attr_cloned_or::(index, DEFAULT_FONT_SIZE); + let line_height = list.attr_cloned_or::(index, 1.2); + let letter_spacing = list.attr_cloned_or_default::(index); + let max_width = list.attr_cloned_or_default::(index); + let max_height = list.attr_cloned_or_default::(index); + let align = list.attr_cloned_or_default::(index); + let transform = list.attr_cloned_or_default::(index); let typesetting = text_nodes::TypesettingConfig { font_size, @@ -2375,7 +2374,7 @@ pub fn graphic_list_bounding_box(list: &List, transform: DAffine2) -> R let mut any_infinite = false; for index in 0..list.len() { - let item_transform = transform * list.attribute_cloned_or_default::(ATTR_TRANSFORM, index); + let item_transform = transform * list.attr_cloned_or_default::(index); let Some(graphic) = list.element(index) else { continue }; let bounds = match graphic { Graphic::Text(text_list) => text_list_bounding_box(text_list, item_transform), @@ -2409,21 +2408,21 @@ impl Render for List { continue; } - let transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index); - let opacity_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY, index, 1.); - let opacity_fill_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY_FILL, index, 1.); - let blend_mode_attr: BlendMode = self.attribute_cloned_or_default(ATTR_BLEND_MODE, index); + let transform = self.attr_cloned_or_default::(index); + let opacity_attr = self.attr_cloned_or_default::(index); + let opacity_fill_attr = self.attr_cloned_or_default::(index); + let blend_mode_attr = self.attr_cloned_or_default::(index); let font: Resource = { - let f: Resource = self.attribute_cloned_or_default(ATTR_FONT, index); + let f = self.attr_cloned_or_default::(index); if f.is_empty() { text_nodes::FALLBACK_FONT_RESOURCE.clone() } else { f } }; - let font_size: f64 = self.attribute_cloned_or(ATTR_FONT_SIZE, index, DEFAULT_FONT_SIZE); - let line_height: f64 = self.attribute_cloned_or(ATTR_LINE_HEIGHT, index, 1.2); - let letter_spacing: f64 = self.attribute_cloned_or(ATTR_LETTER_SPACING, index, 0.); - let max_width: Option = self.attribute_cloned_or(ATTR_MAX_WIDTH, index, None); - let max_height: Option = self.attribute_cloned_or(ATTR_MAX_HEIGHT, index, None); - let letter_tilt: f64 = self.attribute_cloned_or(ATTR_LETTER_TILT, index, 0.); - let align: text_nodes::TextAlign = self.attribute_cloned_or_default(ATTR_TEXT_ALIGN, index); + let font_size = self.attr_cloned_or::(index, DEFAULT_FONT_SIZE); + let line_height = self.attr_cloned_or::(index, 1.2); + let letter_spacing = self.attr_cloned_or_default::(index); + let max_width = self.attr_cloned_or_default::(index); + let max_height = self.attr_cloned_or_default::(index); + let letter_tilt = self.attr_cloned_or_default::(index); + let align = self.attr_cloned_or_default::(index); let opacity = (opacity_attr * if render_params.for_mask { 1. } else { opacity_fill_attr }) as f32; let typesetting = text_nodes::TypesettingConfig { @@ -2494,21 +2493,21 @@ impl Render for List { continue; } - let item_transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index); + let item_transform = self.attr_cloned_or_default::(index); let font: Resource = { - let f: Resource = self.attribute_cloned_or_default(ATTR_FONT, index); + let f = self.attr_cloned_or_default::(index); if f.is_empty() { text_nodes::FALLBACK_FONT_RESOURCE.clone() } else { f } }; - let font_size: f64 = self.attribute_cloned_or(ATTR_FONT_SIZE, index, DEFAULT_FONT_SIZE); - let line_height: f64 = self.attribute_cloned_or(ATTR_LINE_HEIGHT, index, 1.2); - let letter_spacing: f64 = self.attribute_cloned_or(ATTR_LETTER_SPACING, index, 0.); - let max_width: Option = self.attribute_cloned_or(ATTR_MAX_WIDTH, index, None); - let max_height: Option = self.attribute_cloned_or(ATTR_MAX_HEIGHT, index, None); - let letter_tilt: f64 = self.attribute_cloned_or(ATTR_LETTER_TILT, index, 0.); - let align: text_nodes::TextAlign = self.attribute_cloned_or_default(ATTR_TEXT_ALIGN, index); - let blend_mode_attr: BlendMode = self.attribute_cloned_or_default(ATTR_BLEND_MODE, index); - let opacity_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY, index, 1.); - let opacity_fill_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY_FILL, index, 1.); + let font_size = self.attr_cloned_or::(index, DEFAULT_FONT_SIZE); + let line_height = self.attr_cloned_or::(index, 1.2); + let letter_spacing = self.attr_cloned_or_default::(index); + let max_width = self.attr_cloned_or_default::(index); + let max_height = self.attr_cloned_or_default::(index); + let letter_tilt = self.attr_cloned_or_default::(index); + let align = self.attr_cloned_or_default::(index); + let blend_mode_attr = self.attr_cloned_or_default::(index); + let opacity_attr = self.attr_cloned_or_default::(index); + let opacity_fill_attr = self.attr_cloned_or_default::(index); let opacity = (opacity_attr * if render_params.for_mask { 1. } else { opacity_fill_attr }) as f32; let typesetting = text_nodes::TypesettingConfig { @@ -2559,7 +2558,7 @@ impl Render for List { fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, caller_element_id: Option) { // Click targets are baked relative to item 0's transform, which `Graphic::collect_metadata` records as `local_transforms[element_id]`. let item_zero_transform: DAffine2 = if !self.is_empty() { - self.attribute_cloned_or_default(ATTR_TRANSFORM, 0) + self.attr_cloned_or_default::(0) } else { DAffine2::IDENTITY }; @@ -2572,7 +2571,7 @@ impl Render for List { let mut accumulated_click_targets: HashMap>> = HashMap::new(); for index in 0..self.len() { - let layer_path: List = self.attribute_cloned_or_default::(ATTR_EDITOR_LAYER_PATH, index).0; + let layer_path: List = self.attr_cloned_or_default::(index).0; let layer = layer_path.iter_element_values().next_back().copied(); let Some(element_id) = caller_element_id.or(layer) else { continue }; diff --git a/node-graph/nodes/blending/src/lib.rs b/node-graph/nodes/blending/src/lib.rs index e45930a06a..4209b7605d 100644 --- a/node-graph/nodes/blending/src/lib.rs +++ b/node-graph/nodes/blending/src/lib.rs @@ -1,6 +1,7 @@ +use core_types::attr; use core_types::list::Item; use core_types::registry::types::Percentage; -use core_types::{ATTR_BLEND_MODE, ATTR_CLIPPING_MASK, ATTR_OPACITY, ATTR_OPACITY_FILL, BlendMode, Color, Ctx}; +use core_types::{BlendMode, Color, Ctx}; use graphic_types::Graphic; use graphic_types::Vector; use graphic_types::raster_types::{CPU, GPU, Raster}; @@ -19,7 +20,7 @@ fn blend_mode( let mut content = content; let blend_mode = *blend_mode.element(); - content.set_attribute(ATTR_BLEND_MODE, blend_mode); + content.set_attr::(blend_mode); content } @@ -54,13 +55,13 @@ fn opacity( let (has_opacity, opacity, has_fill, fill) = (*has_opacity.element(), *opacity.element(), *has_fill.element(), *fill.element()); if has_opacity { - let multiplied = content.attribute_cloned_or(ATTR_OPACITY, 1.) * (opacity / 100.); - content.set_attribute(ATTR_OPACITY, multiplied); + let multiplied = content.attr_cloned_or_default::() * (opacity / 100.); + content.set_attr::(multiplied); } if has_fill { - let multiplied = content.attribute_cloned_or(ATTR_OPACITY_FILL, 1.) * (fill / 100.); - content.set_attribute(ATTR_OPACITY_FILL, multiplied); + let multiplied = content.attr_cloned_or_default::() * (fill / 100.); + content.set_attr::(multiplied); } content @@ -79,6 +80,6 @@ fn clipping_mask( let mut content = content; let clip = *clip.element(); - content.set_attribute(ATTR_CLIPPING_MASK, clip); + content.set_attr::(clip); content } diff --git a/node-graph/nodes/brush/src/brush.rs b/node-graph/nodes/brush/src/brush.rs index 402c8aec9a..90c887f924 100644 --- a/node-graph/nodes/brush/src/brush.rs +++ b/node-graph/nodes/brush/src/brush.rs @@ -1,6 +1,6 @@ use crate::brush_cache::BrushCache; use crate::brush_stroke::{BrushStyle, BrushTrace}; -use core_types::ATTR_TRANSFORM; +use core_types::attr; use core_types::blending::BlendMode; use core_types::bounds::{BoundingBox, RenderBoundingBox}; use core_types::color::{Alpha, Color, Pixel, Sample}; @@ -91,7 +91,7 @@ where return target; } - let (elements, transforms) = target.element_and_attribute_slices_mut::(ATTR_TRANSFORM); + let (elements, transforms) = target.element_and_attr_slices_mut::(); for (element, transform_attribute) in elements.iter_mut().zip(transforms.iter()) { let target_width = element.width; let target_height = element.height; @@ -281,7 +281,7 @@ async fn brush( let has_erase_or_restore_strokes = trace.iter_element_values().any(|s| matches!(s.style.blend_mode, BlendMode::Erase | BlendMode::Restore)); if has_erase_or_restore_strokes { let opaque_image = Image::new(bbox.size().x as u32, bbox.size().y as u32, Color::WHITE); - let mut erase_restore_mask = Item::new_from_element(Raster::new_cpu(opaque_image)).with_attribute(ATTR_TRANSFORM, background_bounds); + let mut erase_restore_mask = Item::new_from_element(Raster::new_cpu(opaque_image)).with_attr::(background_bounds); for stroke in trace.into_iter().map(|row| row.into_element()) { let mut brush_texture = cache.get_cached_brush(&stroke.style); @@ -315,10 +315,10 @@ async fn brush( // The paint operation changes only the raster and its bounds, so set just the resulting transform; blending, opacity, // clipping, and layer-path attributes carry through from the input `background` rather than being invented here. - let transform: DAffine2 = actual_image.attribute_cloned_or_default(ATTR_TRANSFORM); + let transform = actual_image.attr_cloned_or_default::(); *result_item.element_mut() = actual_image.into_element(); - result_item.set_attribute(ATTR_TRANSFORM, transform); + result_item.set_attr::(transform); result_item } @@ -328,8 +328,8 @@ pub fn blend_image_closure(foreground: Item>, mut background: Item(); + let background_transform = background.attr_cloned_or_default::(); let background_to_foreground = DAffine2::from_scale(foreground_size) * foreground_transform.inverse() * background_transform * DAffine2::from_scale(1. / background_size); // Footprint of the foreground image (0, 0)..(1, 1) in the background image space @@ -360,7 +360,7 @@ pub fn blend_stamp_closure(foreground: BrushStampGenerator, mut backgroun let background_size = DVec2::new(background.element().width as f64, background.element().height as f64); // Transforms a point from the background image to the foreground image - let background_transform: DAffine2 = background.attribute_cloned_or_default(ATTR_TRANSFORM); + let background_transform = background.attr_cloned_or_default::(); let background_to_foreground = background_transform * DAffine2::from_scale(1. / background_size); // Footprint of the foreground image (0, 0)..(1, 1) in the background image space diff --git a/node-graph/nodes/brush/src/brush_cache.rs b/node-graph/nodes/brush/src/brush_cache.rs index f4b980efb8..c5e9419bdf 100644 --- a/node-graph/nodes/brush/src/brush_cache.rs +++ b/node-graph/nodes/brush/src/brush_cache.rs @@ -1,6 +1,6 @@ use crate::brush_stroke::BrushStroke; use crate::brush_stroke::BrushStyle; -use core_types::ATTR_TRANSFORM; +use core_types::attr; use core_types::graphene_hash::CacheHashWrapper; use core_types::list::Item; use raster_types::CPU; @@ -51,7 +51,7 @@ impl BrushCacheImpl { // Check if the first non-blended stroke is an extension of the last one. // Transform is set to ZERO (not the default IDENTITY) as a sentinel to mark this item as uninitialized. - let mut first_stroke_texture = Item::new_from_element(Raster::::default()).with_attribute(ATTR_TRANSFORM, glam::DAffine2::ZERO); + let mut first_stroke_texture = Item::new_from_element(Raster::::default()).with_attr::(glam::DAffine2::ZERO); let mut first_stroke_point_skip = 0; let strokes = input[num_blended_strokes..].to_vec(); if !strokes.is_empty() && self.prev_input.len() > num_blended_strokes { diff --git a/node-graph/nodes/graphic/src/artboard.rs b/node-graph/nodes/graphic/src/artboard.rs index fd61bf93b6..4e12ba44d1 100644 --- a/node-graph/nodes/graphic/src/artboard.rs +++ b/node-graph/nodes/graphic/src/artboard.rs @@ -1,6 +1,7 @@ +use core_types::attr; use core_types::list::{Item, List}; use core_types::transform::TransformMut; -use core_types::{ATTR_BACKGROUND, ATTR_CLIP, ATTR_DIMENSIONS, ATTR_LOCATION, CloneVarArgs, Color, Context, Ctx, ExtractAll, OwnedContextImpl}; +use core_types::{CloneVarArgs, Color, Context, Ctx, ExtractAll, OwnedContextImpl}; use glam::{DAffine2, DVec2}; use graphic_types::graphic::{Graphic, IntoGraphicList}; use graphic_types::{Artboard, Vector}; @@ -53,8 +54,8 @@ pub async fn create_artboard( // Name is not stored here, it's resolved live from the parent layer's display name Item::new_from_element(Artboard::new(content)) - .with_attribute(ATTR_LOCATION, normalized_location) - .with_attribute(ATTR_DIMENSIONS, normalized_dimensions) - .with_attribute(ATTR_BACKGROUND, background) - .with_attribute(ATTR_CLIP, clip) + .with_attr::(normalized_location) + .with_attr::(normalized_dimensions) + .with_attr::(background) + .with_attr::(clip) } diff --git a/node-graph/nodes/graphic/src/graphic.rs b/node-graph/nodes/graphic/src/graphic.rs index 13fa29927f..8c481dae0f 100644 --- a/node-graph/nodes/graphic/src/graphic.rs +++ b/node-graph/nodes/graphic/src/graphic.rs @@ -1,7 +1,8 @@ +use core_types::attr; use core_types::bounds::{BoundingBox, RenderBoundingBox}; use core_types::list::{AttributeValueDyn, Item, List, ListDyn, NodeIdPath}; use core_types::registry::types::{Angle, SeedValue, SignedInteger}; -use core_types::{ATTR_EDITOR_LAYER_PATH, ATTR_EDITOR_MERGED_LAYERS, ATTR_TRANSFORM, AnyHash, BlendMode, CacheHash, CloneVarArgs, Color, Context, Ctx, ExtractAll, OwnedContextImpl}; +use core_types::{AnyHash, BlendMode, CacheHash, CloneVarArgs, Color, Context, Ctx, ExtractAll, OwnedContextImpl}; use glam::{DAffine2, DVec2}; use graphic_types::graphic::{Graphic, IntoGraphicList}; use graphic_types::{Artboard, Vector}; @@ -489,7 +490,7 @@ async fn mirror( let normal = DVec2::from_angle(angle.to_radians()); // The mirror reference may be based on the bounding box if an explicit reference point is chosen - let item_transform: DAffine2 = content.attribute_cloned_or_default(ATTR_TRANSFORM); + let item_transform = content.attr_cloned_or_default::(); let RenderBoundingBox::Rectangle(bounding_box) = content.element().bounding_box(item_transform, false) else { return List::new_from_item(content); }; @@ -521,7 +522,7 @@ async fn mirror( // Add the mirrored copy with the reflection composed onto its transform let mut mirrored = content; - mirrored.set_attribute(ATTR_TRANSFORM, reflected_transform * item_transform); + mirrored.set_attr::(reflected_transform * item_transform); result_list.push(mirrored); result_list @@ -600,7 +601,7 @@ fn read_attribute_vector( let name = name.into_element(); let mut result = List::with_capacity(content.len()); for index in 0..content.len() { - let Some(value) = content.attribute::(&name, index) else { continue }; + let Some(value) = content.attribute_dyn::(&name, index) else { continue }; result.push(Item::new_from_element(value.clone())); } result @@ -618,10 +619,10 @@ fn read_attribute_number( let mut result = List::with_capacity(content.len()); for index in 0..content.len() { let value = content - .attribute::(&name, index) + .attribute_dyn::(&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)); + .or_else(|| content.attribute_dyn::(&name, index).map(|v| *v as f64)) + .or_else(|| content.attribute_dyn::(&name, index).map(|v| *v as f64)); let Some(value) = value else { continue }; result.push(Item::new_from_element(value)); } @@ -639,7 +640,7 @@ fn read_attribute_bool( let name = name.into_element(); let mut result = List::with_capacity(content.len()); for index in 0..content.len() { - let Some(value) = content.attribute::(&name, index) else { continue }; + let Some(value) = content.attribute_dyn::(&name, index) else { continue }; result.push(Item::new_from_element(*value)); } result @@ -656,7 +657,7 @@ fn read_attribute_string( let name = name.into_element(); let mut result = List::with_capacity(content.len()); for index in 0..content.len() { - let Some(value) = content.attribute::(&name, index) else { continue }; + let Some(value) = content.attribute_dyn::(&name, index) else { continue }; result.push(Item::new_from_element(value.clone())); } result @@ -673,7 +674,7 @@ fn read_attribute_transform( let name = name.into_element(); let mut result = List::with_capacity(content.len()); for index in 0..content.len() { - let Some(value) = content.attribute::(&name, index) else { continue }; + let Some(value) = content.attribute_dyn::(&name, index) else { continue }; result.push(Item::new_from_element(*value)); } result @@ -690,7 +691,7 @@ fn read_attribute_color( let name = name.into_element(); let mut result = List::with_capacity(content.len()); for index in 0..content.len() { - let Some(value) = content.attribute::(&name, index) else { continue }; + let Some(value) = content.attribute_dyn::(&name, index) else { continue }; result.push(Item::new_from_element(*value)); } result @@ -707,7 +708,7 @@ fn read_attribute_blend_mode( let name = name.into_element(); let mut result = List::with_capacity(content.len()); for index in 0..content.len() { - let Some(value) = content.attribute::(&name, index) else { continue }; + let Some(value) = content.attribute_dyn::(&name, index) else { continue }; result.push(Item::new_from_element(*value)); } result @@ -724,7 +725,7 @@ fn read_attribute_gradient_type( let name = name.into_element(); let mut result = List::with_capacity(content.len()); for index in 0..content.len() { - let Some(value) = content.attribute::(&name, index) else { continue }; + let Some(value) = content.attribute_dyn::(&name, index) else { continue }; result.push(Item::new_from_element(*value)); } result @@ -741,7 +742,7 @@ fn read_attribute_spread_method( let name = name.into_element(); let mut result = List::with_capacity(content.len()); for index in 0..content.len() { - let Some(value) = content.attribute::(&name, index) else { continue }; + let Some(value) = content.attribute_dyn::(&name, index) else { continue }; result.push(Item::new_from_element(*value)); } result @@ -758,7 +759,7 @@ fn read_attribute_gradient_stops( let name = name.into_element(); let mut result = List::with_capacity(content.len()); for index in 0..content.len() { - let Some(value) = content.attribute::(&name, index) else { continue }; + let Some(value) = content.attribute_dyn::(&name, index) else { continue }; result.push(Item::new_from_element(value.clone())); } result @@ -775,7 +776,7 @@ fn read_attribute_artboard( let name = name.into_element(); let mut result = List::with_capacity(content.len()); for index in 0..content.len() { - let Some(value) = content.attribute::(&name, index) else { continue }; + let Some(value) = content.attribute_dyn::(&name, index) else { continue }; result.push(Item::new_from_element(value.clone())); } result @@ -792,7 +793,7 @@ fn read_attribute_raster( let name = name.into_element(); let mut result = List::with_capacity(content.len()); for index in 0..content.len() { - let Some(value) = content.attribute::>(&name, index) else { continue }; + let Some(value) = content.attribute_dyn::>(&name, index) else { continue }; result.push(Item::new_from_element(value.clone())); } result @@ -869,7 +870,7 @@ pub async fn legacy_layer_extend( let mut base = base; for mut row in new.into_iter() { - row.set_attribute(ATTR_EDITOR_LAYER_PATH, layer_path.clone()); + row.set_attr::(layer_path.clone()); base.push(row); } @@ -926,7 +927,7 @@ pub async fn flatten_graphic(_: impl Ctx, content: List, fully_flatten: for index in 0..current_graphic_list.len() { let Some(current_element) = current_graphic_list.element(index) else { continue }; let current_element = current_element.clone(); - let current_transform: DAffine2 = current_graphic_list.attribute_cloned_or_default(ATTR_TRANSFORM, index); + let current_transform = current_graphic_list.attr_cloned_or_default::(index); let recurse = fully_flatten || recursion_depth == 0; @@ -934,7 +935,7 @@ pub async fn flatten_graphic(_: impl Ctx, content: List, fully_flatten: // If we're allowed to recurse, flatten any graphics we encounter Graphic::Graphic(mut current_element) if recurse => { // Apply the parent graphic's transform to all child elements - for graphic_transform in current_element.iter_attribute_values_mut_or_default::(ATTR_TRANSFORM) { + for graphic_transform in current_element.iter_attr_values_mut_or_default::() { *graphic_transform = current_transform * *graphic_transform; } @@ -974,15 +975,15 @@ pub async fn flatten_vector(_: impl Ctx, #[implementations(L // already holds the original transforms; pre-compensate by item 0's inverse so the renderer's // `upstream_footprint *= item_0_transform` recursion cancels out and leaves the originals intact. let mut graphic_list = graphic_list; - let item_0_transform: DAffine2 = output.attribute_cloned_or_default(ATTR_TRANSFORM, 0); + let item_0_transform = output.attr_cloned_or_default::(0); if item_0_transform.matrix2.determinant().abs() > f64::EPSILON { let inverse = item_0_transform.inverse(); - for transform in graphic_list.iter_attribute_values_mut_or_default::(ATTR_TRANSFORM) { + for transform in graphic_list.iter_attr_values_mut_or_default::() { *transform = inverse * *transform; } } - output.set_attribute(ATTR_EDITOR_MERGED_LAYERS, 0, graphic_list); + output.set_attr::(0, graphic_list); } output diff --git a/node-graph/nodes/gstd/src/platform_application_io.rs b/node-graph/nodes/gstd/src/platform_application_io.rs index 996e67de82..6899e6e548 100644 --- a/node-graph/nodes/gstd/src/platform_application_io.rs +++ b/node-graph/nodes/gstd/src/platform_application_io.rs @@ -10,9 +10,9 @@ use core_types::list::List; use core_types::math::bbox::Bbox; use core_types::ops::Convert; use core_types::transform::Footprint; -#[cfg(target_family = "wasm")] -use core_types::{ATTR_EDITOR_MERGED_LAYERS, ATTR_TRANSFORM, WasmNotSend}; use core_types::{Color, Ctx}; +#[cfg(target_family = "wasm")] +use core_types::{WasmNotSend, attr}; pub use graph_craft::application_io::resource::{Resource, ResourceHash}; pub use graph_craft::application_io::*; pub use graph_craft::document::value::RenderOutputType; @@ -243,7 +243,7 @@ where ..Default::default() }; - for transform in data.iter_attribute_values_mut_or_default::(ATTR_TRANSFORM) { + for transform in data.iter_attr_values_mut_or_default::() { *transform = DAffine2::from_translation(-aabb.start) * *transform; } data.render_svg(&mut render, &render_params); @@ -270,8 +270,8 @@ where let image = Image::from_image_data(&rasterized.data().0, resolution.x as u32, resolution.y as u32); List::new_from_item( Item::new_from_element(Raster::new_cpu(image)) - .with_attribute(ATTR_TRANSFORM, footprint.transform) - .with_attribute(ATTR_EDITOR_MERGED_LAYERS, upstream_graphic_list), + .with_attr::(footprint.transform) + .with_attr::(upstream_graphic_list), ) } diff --git a/node-graph/nodes/gstd/src/text.rs b/node-graph/nodes/gstd/src/text.rs index e58f16f590..fc300713e4 100644 --- a/node-graph/nodes/gstd/src/text.rs +++ b/node-graph/nodes/gstd/src/text.rs @@ -1,6 +1,6 @@ +use core_types::Ctx; use core_types::consts::{DEFAULT_FONT_SIZE, DEFAULT_LINE_HEIGHT}; use core_types::list::{Item, List}; -use core_types::{ATTR_FONT, ATTR_FONT_SIZE, ATTR_LETTER_SPACING, ATTR_LETTER_TILT, ATTR_LINE_HEIGHT, ATTR_MAX_HEIGHT, ATTR_MAX_WIDTH, ATTR_TEXT_ALIGN, Ctx}; use graph_craft::application_io::resource::Resource; use graphic_types::Vector; pub use text_nodes::*; @@ -69,28 +69,28 @@ fn text( let mut item = Item::new_from_element(text); if font != Resource::default() { - item.set_attribute(ATTR_FONT, font); + item.set_attr::(font); } if (size - DEFAULT_FONT_SIZE).abs() > f64::EPSILON { - item.set_attribute(ATTR_FONT_SIZE, size); + item.set_attr::(size); } if (line_height - DEFAULT_LINE_HEIGHT).abs() > f64::EPSILON { - item.set_attribute(ATTR_LINE_HEIGHT, line_height); + item.set_attr::(line_height); } if letter_spacing != 0. { - item.set_attribute(ATTR_LETTER_SPACING, letter_spacing); + item.set_attr::(letter_spacing); } if letter_tilt != 0. { - item.set_attribute(ATTR_LETTER_TILT, letter_tilt); + item.set_attr::(letter_tilt); } if has_max_width { - item.set_attribute(ATTR_MAX_WIDTH, Some(max_width)); + item.set_attr::(Some(max_width)); } if has_max_height { - item.set_attribute(ATTR_MAX_HEIGHT, Some(max_height)); + item.set_attr::(Some(max_height)); } if align != TextAlign::default() { - item.set_attribute(ATTR_TEXT_ALIGN, align); + item.set_attr::(align); } item diff --git a/node-graph/nodes/math/src/lib.rs b/node-graph/nodes/math/src/lib.rs index e06911cab2..68c9b7aee4 100644 --- a/node-graph/nodes/math/src/lib.rs +++ b/node-graph/nodes/math/src/lib.rs @@ -1043,7 +1043,7 @@ fn gradient_value(_: impl Ctx, _primary: (), gradient: Item) -> Item, gradient_type: Item) -> Item { let mut gradient = gradient; - gradient.set_attribute(core_types::ATTR_GRADIENT_TYPE, *gradient_type.element()); + gradient.set_attr::(*gradient_type.element()); gradient } @@ -1051,7 +1051,7 @@ fn gradient_type(_: impl Ctx, gradient: Item, gradient_type: Item, spread_method: Item) -> Item { let mut gradient = gradient; - gradient.set_attribute(core_types::ATTR_SPREAD_METHOD, *spread_method.element()); + gradient.set_attr::(*spread_method.element()); gradient } diff --git a/node-graph/nodes/path-bool/src/lib.rs b/node-graph/nodes/path-bool/src/lib.rs index bcfed58a5a..b675910da7 100644 --- a/node-graph/nodes/path-bool/src/lib.rs +++ b/node-graph/nodes/path-bool/src/lib.rs @@ -1,10 +1,8 @@ -use core_types::list::{ATTR_FILL, Item, ItemAttributeValues, List}; -use core_types::{ - ATTR_BLEND_MODE, ATTR_CLIPPING_MASK, ATTR_EDITOR_LAYER_PATH, ATTR_EDITOR_MERGED_LAYERS, ATTR_GRADIENT_TYPE, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_SPREAD_METHOD, ATTR_TRANSFORM, Color, Ctx, -}; +use core_types::attr::{self, Attr}; +use core_types::list::{Item, ItemAttributeValues, List}; +use core_types::{Color, Ctx}; use glam::{DAffine2, DVec2}; use graphic_types::graphic::{bake_paint_transforms, set_paint_attribute}; -use graphic_types::vector_types::gradient::{GradientSpreadMethod, GradientType}; use graphic_types::vector_types::subpath::{ManipulatorGroup, Subpath}; use graphic_types::vector_types::vector::PointId; use graphic_types::vector_types::vector::algorithms::merge_by_distance::MergeByDistanceExt; @@ -43,8 +41,8 @@ async fn boolean_operation( // Replace the transformation matrix with a mutation of the vector points themselves if result_vector_list.element_mut(0).is_some() { - let transform: DAffine2 = result_vector_list.attribute_cloned_or_default(ATTR_TRANSFORM, 0); - result_vector_list.set_attribute(ATTR_TRANSFORM, 0, DAffine2::IDENTITY); + let transform = result_vector_list.attr_cloned_or_default::(0); + result_vector_list.set_attr::(0, DAffine2::IDENTITY); let result_vector = result_vector_list.element_mut(0).unwrap(); Vector::transform(result_vector, transform); @@ -52,10 +50,10 @@ async fn boolean_operation( // Snapshot the input layers as the `editor:merged_layers` attribute so the renderer can recurse into them // for editor click-target preservation. - result_vector_list.set_attribute(ATTR_EDITOR_MERGED_LAYERS, 0, content.clone()); + result_vector_list.set_attr::(0, content.clone()); // Clean up the boolean operation result by merging duplicated points - let merge_transform: DAffine2 = result_vector_list.attribute_cloned_or_default(ATTR_TRANSFORM, 0); + let merge_transform = result_vector_list.attr_cloned_or_default::(0); result_vector_list.element_mut(0).unwrap().merge_by_distance_spatial(merge_transform, 0.0001); } @@ -139,9 +137,9 @@ fn boolean_operation_on_vector_list(vector: &List, boolean_operation: Bo }; let mut row = if let Some(index) = copy_from_index { let mut attributes = vector.clone_item_attributes(index); - let copy_from_transform: DAffine2 = vector.attribute_cloned_or_default(ATTR_TRANSFORM, index); + let copy_from_transform = vector.attr_cloned_or_default::(index); // The boolean op bakes input transforms into the output geometry, so the result item carries no transform of its own - attributes.insert(ATTR_TRANSFORM, DAffine2::IDENTITY); + attributes.set_attr::(DAffine2::IDENTITY); bake_paint_transforms(&mut attributes, copy_from_transform); @@ -157,7 +155,7 @@ fn boolean_operation_on_vector_list(vector: &List, boolean_operation: Bo for index in 0..vector.len() { let element = vector.element(index).unwrap(); - paths.push(to_bez_path(element, vector.attribute_cloned_or_default(ATTR_TRANSFORM, index))); + paths.push(to_bez_path(element, vector.attr_cloned_or_default::(index))); } let top = match Topology::::from_paths(paths.iter().enumerate().map(|(idx, path)| (path, (idx, paths.len()))), EPSILON) { @@ -185,18 +183,18 @@ fn flatten_vector(graphic_list: &List) -> List { Graphic::None => Vec::new(), Graphic::Vector(vector) => { // Apply the parent graphic's transform to each element of the `List` - let parent_transform: DAffine2 = graphic_list.attribute_cloned_or_default(ATTR_TRANSFORM, index); + let parent_transform = graphic_list.attr_cloned_or_default::(index); vector .into_iter() .map(|mut sub_vector| { - let current_transform: DAffine2 = sub_vector.attribute_cloned_or_default(ATTR_TRANSFORM); - *sub_vector.attribute_mut_or_insert_default(ATTR_TRANSFORM) = parent_transform * current_transform; + let current_transform = sub_vector.attr_cloned_or_default::(); + *sub_vector.attr_mut_or_insert_default::() = parent_transform * current_transform; sub_vector }) .collect::>() } Graphic::RasterCPU(image) => { - let parent_transform: DAffine2 = graphic_list.attribute_cloned_or_default(ATTR_TRANSFORM, index); + let parent_transform = graphic_list.attr_cloned_or_default::(index); let make_item = |transform: DAffine2, source_attributes: &ItemAttributeValues| { let mut subpath = Subpath::new_rectangle(DVec2::ZERO, DVec2::ONE); subpath.apply_transform(transform); @@ -204,10 +202,16 @@ fn flatten_vector(graphic_list: &List) -> List { let element = Vector::from_subpath(subpath); let mut item = Item::new_from_element(element); - for key in [ATTR_BLEND_MODE, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_CLIPPING_MASK, ATTR_EDITOR_LAYER_PATH] { + for key in [ + attr::BlendMode::name(), + attr::Opacity::name(), + attr::OpacityFill::name(), + attr::ClippingMask::name(), + attr::editor::LayerPath::name(), + ] { item.attributes_mut().insert_cloned_from(source_attributes, key); } - set_paint_attribute(item.attributes_mut(), ATTR_FILL, List::new_from_element(Color::BLACK)); + set_paint_attribute::(item.attributes_mut(), List::new_from_element(Color::BLACK)); item }; @@ -216,14 +220,14 @@ fn flatten_vector(graphic_list: &List) -> List { // back to the originating raster layer (0..image.len()) .map(|i| { - let row_transform: DAffine2 = image.attribute_cloned_or_default(ATTR_TRANSFORM, i); + let row_transform = image.attr_cloned_or_default::(i); let source_attributes = image.clone_item_attributes(i); make_item(parent_transform * row_transform, &source_attributes) }) .collect::>() } Graphic::RasterGPU(image) => { - let parent_transform: DAffine2 = graphic_list.attribute_cloned_or_default(ATTR_TRANSFORM, index); + let parent_transform = graphic_list.attr_cloned_or_default::(index); let make_item = |transform: DAffine2, source_attributes: &ItemAttributeValues| { let mut subpath = Subpath::new_rectangle(DVec2::ZERO, DVec2::ONE); subpath.apply_transform(transform); @@ -231,10 +235,16 @@ fn flatten_vector(graphic_list: &List) -> List { let element = Vector::from_subpath(subpath); let mut item = Item::new_from_element(element); - for key in [ATTR_BLEND_MODE, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_CLIPPING_MASK, ATTR_EDITOR_LAYER_PATH] { + for key in [ + attr::BlendMode::name(), + attr::Opacity::name(), + attr::OpacityFill::name(), + attr::ClippingMask::name(), + attr::editor::LayerPath::name(), + ] { item.attributes_mut().insert_cloned_from(source_attributes, key); } - set_paint_attribute(item.attributes_mut(), ATTR_FILL, List::new_from_element(Color::BLACK)); + set_paint_attribute::(item.attributes_mut(), List::new_from_element(Color::BLACK)); item }; @@ -243,16 +253,16 @@ fn flatten_vector(graphic_list: &List) -> List { // back to the originating raster layer (0..image.len()) .map(|i| { - let row_transform: DAffine2 = image.attribute_cloned_or_default(ATTR_TRANSFORM, i); + let row_transform = image.attr_cloned_or_default::(i); let source_attributes = image.clone_item_attributes(i); make_item(parent_transform * row_transform, &source_attributes) }) .collect::>() } Graphic::Graphic(mut graphic) => { - let parent_transform: DAffine2 = graphic_list.attribute_cloned_or_default(ATTR_TRANSFORM, index); + let parent_transform = graphic_list.attr_cloned_or_default::(index); // Apply the parent graphic's transform to each element of the inner `List` - for transform in graphic.iter_attribute_values_mut_or_default::(ATTR_TRANSFORM) { + for transform in graphic.iter_attr_values_mut_or_default::() { *transform = parent_transform * *transform; } @@ -266,7 +276,7 @@ fn flatten_vector(graphic_list: &List) -> List { .into_iter() .map(|row| { let (color, mut attributes) = row.into_parts(); - set_paint_attribute(&mut attributes, ATTR_FILL, List::new_from_element(color)); + set_paint_attribute::(&mut attributes, List::new_from_element(color)); let mut element = Vector::default(); element.set_stroke_transform(DAffine2::IDENTITY); @@ -280,16 +290,16 @@ fn flatten_vector(graphic_list: &List) -> List { let (stops, mut attributes) = row.into_parts(); let mut gradient_paint = List::new_from_element(stops); - if let Some(transform) = attributes.remove::(ATTR_TRANSFORM) { - gradient_paint.set_attribute(ATTR_TRANSFORM, 0, transform); + if let Some(transform) = attributes.remove_attr::() { + gradient_paint.set_attr::(0, transform); } - if let Some(gradient_type) = attributes.remove::(ATTR_GRADIENT_TYPE) { - gradient_paint.set_attribute(ATTR_GRADIENT_TYPE, 0, gradient_type); + if let Some(gradient_type) = attributes.remove_attr::() { + gradient_paint.set_attr::(0, gradient_type); } - if let Some(spread_method) = attributes.remove::(ATTR_SPREAD_METHOD) { - gradient_paint.set_attribute(ATTR_SPREAD_METHOD, 0, spread_method); + if let Some(spread_method) = attributes.remove_attr::() { + gradient_paint.set_attr::(0, spread_method); } - set_paint_attribute(&mut attributes, ATTR_FILL, gradient_paint); + set_paint_attribute::(&mut attributes, gradient_paint); let mut element = Vector::default(); element.set_stroke_transform(DAffine2::IDENTITY); @@ -299,12 +309,12 @@ fn flatten_vector(graphic_list: &List) -> List { .collect::>(), Graphic::Text(text) => { // Shape the glyphs into vectors (each item's own transform is applied), then compose the parent's transform like the other arms - let parent_transform: DAffine2 = graphic_list.attribute_cloned_or_default(ATTR_TRANSFORM, index); + let parent_transform = graphic_list.attr_cloned_or_default::(index); text_nodes::shape_text_list(&text, false) .into_iter() .map(|mut sub_vector| { - let current_transform: DAffine2 = sub_vector.attribute_cloned_or_default(ATTR_TRANSFORM); - *sub_vector.attribute_mut_or_insert_default(ATTR_TRANSFORM) = parent_transform * current_transform; + let current_transform = sub_vector.attr_cloned_or_default::(); + *sub_vector.attr_mut_or_insert_default::() = parent_transform * current_transform; sub_vector }) .collect::>() diff --git a/node-graph/nodes/raster/src/std_nodes.rs b/node-graph/nodes/raster/src/std_nodes.rs index 2662cfbfd3..611a28716d 100644 --- a/node-graph/nodes/raster/src/std_nodes.rs +++ b/node-graph/nodes/raster/src/std_nodes.rs @@ -1,5 +1,5 @@ use crate::adjustments::{CellularDistanceFunction, CellularReturnType, DomainWarpType, FractalType, NoiseType}; -use core_types::ATTR_TRANSFORM; +use core_types::attr; use core_types::color::Color; use core_types::color::{Alpha, AlphaMut, Channel, LinearChannel, Luminance, RGBMut}; use core_types::context::{Ctx, ExtractFootprint}; @@ -32,7 +32,7 @@ impl From for Error { #[node_macro::node(category("Debug"))] pub fn sample_image(ctx: impl ExtractFootprint + Clone + Send, image_frame: Item>) -> Item> { - let image_frame_transform: DAffine2 = image_frame.attribute_cloned_or_default(ATTR_TRANSFORM); + let image_frame_transform = image_frame.attr_cloned_or_default::(); let footprint = ctx.footprint(); let viewport_bounds = footprint.viewport_bounds_in_local_space(); @@ -86,7 +86,7 @@ pub fn sample_image(ctx: impl ExtractFootprint + Clone + Send, image_frame: Item // we need to adjust the offset if we truncate the offset calculation let new_transform = image_frame_transform * DAffine2::from_translation(offset) * DAffine2::from_scale(size); - attributes.insert(ATTR_TRANSFORM, new_transform); + attributes.set_attr::(new_transform); Item::from_parts(Raster::new_cpu(image), attributes) } @@ -163,7 +163,7 @@ pub fn mask( let mut row = image; let image_size = DVec2::new(row.element().width as f64, row.element().height as f64); - let stencil_transform: DAffine2 = stencil.attribute_cloned_or_default(ATTR_TRANSFORM); + let stencil_transform = stencil.attr_cloned_or_default::(); let mask_size = stencil_transform.scale_magnitudes(); if mask_size == DVec2::ZERO { @@ -171,7 +171,7 @@ pub fn mask( } // Transforms a point from the background image to the foreground image - let transform_attribute: DAffine2 = row.attribute_cloned_or_default(ATTR_TRANSFORM); + let transform_attribute = row.attr_cloned_or_default::(); let bg_to_fg = transform_attribute * DAffine2::from_scale(1. / image_size); let stencil_transform_inverse = stencil_transform.inverse(); @@ -196,7 +196,7 @@ pub fn mask( pub fn extend_image_to_bounds(_: impl Ctx, image: Item>, bounds: Item) -> Item> { let bounds = *bounds.element(); - let image_transform: DAffine2 = image.attribute_cloned_or_default(ATTR_TRANSFORM); + let image_transform = image.attr_cloned_or_default::(); let image_aabb = Bbox::unit().affine_transform(image_transform).to_axis_aligned_bbox(); let bounds_aabb = Bbox::unit().affine_transform(bounds.transform()).to_axis_aligned_bbox(); if image_aabb.contains(bounds_aabb.start) && image_aabb.contains(bounds_aabb.end) { @@ -232,7 +232,7 @@ pub fn extend_image_to_bounds(_: impl Ctx, image: Item>, bounds: Ite // let layer_to_new_texture_space = (DAffine2::from_scale(1. / new_scale) * DAffine2::from_translation(new_start) * layer_to_image_space).inverse(); let new_texture_to_layer_space = image_transform * DAffine2::from_scale(1. / orig_image_scale) * DAffine2::from_translation(new_start) * DAffine2::from_scale(new_scale); - attributes.insert(ATTR_TRANSFORM, new_texture_to_layer_space); + attributes.set_attr::(new_texture_to_layer_space); Item::from_parts(Raster::new_cpu(new_image), attributes) } @@ -244,7 +244,7 @@ pub fn empty_image(_: impl Ctx, transform: Item, color: Item) - let image = Image::new(width, height, color.into_element()); - Item::new_from_element(Raster::new_cpu(image)).with_attribute(ATTR_TRANSFORM, transform) + Item::new_from_element(Raster::new_cpu(image)).with_attr::(transform) } #[node_macro::node(category(""))] @@ -376,7 +376,7 @@ pub fn noise_pattern( } } - return Item::new_from_element(Raster::new_cpu(image)).with_attribute(ATTR_TRANSFORM, transform); + return Item::new_from_element(Raster::new_cpu(image)).with_attr::(transform); } }; noise.set_noise_type(Some(noise_type)); @@ -434,7 +434,7 @@ pub fn noise_pattern( } } - Item::new_from_element(Raster::new_cpu(image)).with_attribute(ATTR_TRANSFORM, transform) + Item::new_from_element(Raster::new_cpu(image)).with_attr::(transform) } #[node_macro::node(category("Raster: Pattern"))] @@ -478,7 +478,7 @@ pub fn mandelbrot(ctx: impl ExtractFootprint + Send) -> Item> { data, ..Default::default() })) - .with_attribute(ATTR_TRANSFORM, DAffine2::from_translation(offset) * DAffine2::from_scale(size)) + .with_attr::(DAffine2::from_translation(offset) * DAffine2::from_scale(size)) } #[inline(always)] diff --git a/node-graph/nodes/repeat/src/repeat_nodes.rs b/node-graph/nodes/repeat/src/repeat_nodes.rs index a9ebb86c7a..819be0fe91 100644 --- a/node-graph/nodes/repeat/src/repeat_nodes.rs +++ b/node-graph/nodes/repeat/src/repeat_nodes.rs @@ -1,8 +1,9 @@ use crate::gcore::Context; use core::f64::consts::TAU; +use core_types::attr; use core_types::list::{Item, List}; use core_types::registry::types::{Angle, PixelSize}; -use core_types::{ATTR_TRANSFORM, CloneVarArgs, Color, Ctx, ExtractAll, InjectVarArgs, OwnedContextImpl}; +use core_types::{CloneVarArgs, Color, Ctx, ExtractAll, InjectVarArgs, OwnedContextImpl}; use glam::{DAffine2, DVec2}; use graphic_types::{Artboard, Graphic, Vector}; use raster_types::{CPU, GPU, Raster}; @@ -102,10 +103,10 @@ pub async fn repeat_array( for row_index in 0..generated_content.len() { let Some(mut row) = generated_content.clone_item(row_index) else { continue }; - let local_transform: DAffine2 = row.attribute_cloned_or_default(ATTR_TRANSFORM); + let local_transform = row.attr_cloned_or_default::(); let local_translation = DAffine2::from_translation(local_transform.translation); let local_matrix = DAffine2::from_mat2(local_transform.matrix2); - *row.attribute_mut_or_insert_default(ATTR_TRANSFORM) = local_translation * transform * local_matrix; + *row.attr_mut_or_insert_default::() = local_translation * transform * local_matrix; result_list.push(row); } @@ -158,10 +159,10 @@ async fn repeat_radial( for row_index in 0..generated_content.len() { let Some(mut row) = generated_content.clone_item(row_index) else { continue }; - let local_transform: DAffine2 = row.attribute_cloned_or_default(ATTR_TRANSFORM); + let local_transform = row.attr_cloned_or_default::(); let local_translation = DAffine2::from_translation(local_transform.translation); let local_matrix = DAffine2::from_mat2(local_transform.matrix2); - *row.attribute_mut_or_insert_default(ATTR_TRANSFORM) = local_translation * transform * local_matrix; + *row.attr_mut_or_insert_default::() = local_translation * transform * local_matrix; result_list.push(row); } @@ -200,7 +201,7 @@ async fn repeat_on_points( for points_index in 0..points.len() { let Some(points_element) = points.element(points_index) else { continue }; - let transform: DAffine2 = points.attribute_cloned_or_default(ATTR_TRANSFORM, points_index); + let transform = points.attr_cloned_or_default::(points_index); let mut iteration = async |index, point| { let transformed_point = transform.transform_point2(point); @@ -209,7 +210,7 @@ async fn repeat_on_points( let generated_content = content.eval(new_ctx.into_context()).await; for mut generated_row in generated_content.into_iter() { - generated_row.attribute_mut_or_insert_default::(ATTR_TRANSFORM).translation = transformed_point; + generated_row.attr_mut_or_insert_default::().translation = transformed_point; result_list.push(generated_row); } }; @@ -301,7 +302,7 @@ mod test { let bounds = generated .element(index) .unwrap() - .bounding_box_with_transform(generated.attribute_cloned_or_default(ATTR_TRANSFORM, index)) + .bounding_box_with_transform(generated.attr_cloned_or_default::(index)) .unwrap(); assert!(position.abs_diff_eq((bounds[0] + bounds[1]) / 2., 1e-10)); assert_eq!((bounds[1] - bounds[0]).x, position.y); diff --git a/node-graph/nodes/text/src/json.rs b/node-graph/nodes/text/src/json.rs index 05fd69ab3c..248a32fd07 100644 --- a/node-graph/nodes/text/src/json.rs +++ b/node-graph/nodes/text/src/json.rs @@ -1,5 +1,5 @@ use core_types::list::{Item, List}; -use core_types::{ATTR_TYPE, Ctx}; +use core_types::{Ctx, attr}; use serde_json::Value; use crate::unescape_string; @@ -265,7 +265,7 @@ fn query_json_all( let mut results = Vec::new(); resolve_all(&value, &segments, !*unquote_strings.element(), &mut results); - results.into_iter().map(|(text, ty)| Item::new_from_element(text).with_attribute(ATTR_TYPE, ty.to_string())).collect() + results.into_iter().map(|(text, ty)| Item::new_from_element(text).with_attr::(ty.to_string())).collect() } /// A parsed segment of a JSON access path. diff --git a/node-graph/nodes/text/src/path_builder.rs b/node-graph/nodes/text/src/path_builder.rs index facf154c86..433a00c54e 100644 --- a/node-graph/nodes/text/src/path_builder.rs +++ b/node-graph/nodes/text/src/path_builder.rs @@ -1,5 +1,5 @@ +use core_types::attr; use core_types::list::{Item, List}; -use core_types::{ATTR_EDITOR_CLICK_TARGET, ATTR_EDITOR_TEXT_FRAME, ATTR_TRANSFORM}; use glam::{DAffine2, DVec2}; use parley::GlyphRun; use skrifa::GlyphId; @@ -15,13 +15,13 @@ pub struct PathBuilder { origin: DVec2, glyph_subpaths: Vec>, pub vector_list: List, - /// Per-glyph AABBs collected in single-item mode, published as `ATTR_EDITOR_CLICK_TARGET` in `finalize()`. + /// Per-glyph AABBs collected in single-item mode, published as `vector_types::attr::editor::ClickTarget` in `finalize()`. merged_click_target_bboxes: Vec<[DVec2; 2]>, /// Per-glyph baselines, parallel to `merged_click_target_bboxes`. Groups glyphs by line for the widening pass. merged_click_target_baselines: Vec, /// Per-glyph AABBs in glyph-local space (multi-item mode), widened in `finalize()` to fill gaps. per_glyph_bboxes: Vec>, - /// Text frame size, stamped per item as `ATTR_EDITOR_TEXT_FRAME` relative to each item's origin. + /// Text frame size, stamped per item as `attr::editor::TextFrame` relative to each item's origin. text_frame_size: DVec2, /// First glyph's baseline offset (pre-height-filter). Used for the empty placeholder item so /// `local_transforms` stays stable when all glyphs are clipped during a resize drag. @@ -85,8 +85,8 @@ impl PathBuilder { let frame_in_item_local = DAffine2::from_scale_angle_translation(self.text_frame_size, 0., -glyph_offset); let item = Item::new_from_element(Vector::from_subpaths(core::mem::take(&mut self.glyph_subpaths), false)) - .with_attribute(ATTR_TRANSFORM, DAffine2::from_translation(glyph_offset)) - .with_attribute(ATTR_EDITOR_TEXT_FRAME, frame_in_item_local); + .with_attr::(DAffine2::from_translation(glyph_offset)) + .with_attr::(frame_in_item_local); self.vector_list.push(item); // Defer click target creation to `finalize()` where adjacent AABBs get widened @@ -170,8 +170,8 @@ impl PathBuilder { if self.vector_list.is_empty() { let frame_in_item_local = DAffine2::from_scale_angle_translation(self.text_frame_size, 0., -self.first_glyph_offset); let item = Item::new_from_element(Vector::default()) - .with_attribute(ATTR_TRANSFORM, DAffine2::from_translation(self.first_glyph_offset)) - .with_attribute(ATTR_EDITOR_TEXT_FRAME, frame_in_item_local); + .with_attr::(DAffine2::from_translation(self.first_glyph_offset)) + .with_attr::(frame_in_item_local); self.vector_list.push(item); } @@ -184,7 +184,7 @@ impl PathBuilder { .enumerate() .filter_map(|(index, bbox)| { let bbox = (*bbox)?; - let offset = self.vector_list.attribute_cloned_or_default::(ATTR_TRANSFORM, index).translation; + let offset = self.vector_list.attr_cloned_or_default::(index).translation; Some((index, offset, [bbox[0] + offset, bbox[1] + offset])) }) .collect(); @@ -197,7 +197,7 @@ impl PathBuilder { for (entry, widened) in entries.iter().zip(layer_bboxes.iter()) { let glyph_local = [widened[0] - entry.1, widened[1] - entry.1]; let rect = Subpath::new_rectangle(glyph_local[0], glyph_local[1]); - self.vector_list.set_attribute(ATTR_EDITOR_CLICK_TARGET, entry.0, Vector::from_subpaths([rect], false)); + self.vector_list.set_attr::(entry.0, Vector::from_subpaths([rect], false)); } } @@ -207,14 +207,14 @@ impl PathBuilder { widen_horizontal_gaps(&mut bboxes, &self.merged_click_target_baselines); let widened_subpaths: Vec<_> = bboxes.iter().map(|[min, max]| Subpath::new_rectangle(*min, *max)).collect(); - self.vector_list.set_attribute(ATTR_EDITOR_CLICK_TARGET, 0, Vector::from_subpaths(widened_subpaths, false)); + self.vector_list.set_attr::(0, Vector::from_subpaths(widened_subpaths, false)); } // Fill in text frame for items that don't have one yet (single-item mode, where item 0 = identity) let frame = DAffine2::from_scale(self.text_frame_size); for index in 0..self.vector_list.len() { - if self.vector_list.attribute::(ATTR_EDITOR_TEXT_FRAME, index).is_none() { - self.vector_list.set_attribute(ATTR_EDITOR_TEXT_FRAME, index, frame); + if self.vector_list.attr::(index).is_none() { + self.vector_list.set_attr::(index, frame); } } diff --git a/node-graph/nodes/text/src/regex.rs b/node-graph/nodes/text/src/regex.rs index 80d412be94..d78cb1fb26 100644 --- a/node-graph/nodes/text/src/regex.rs +++ b/node-graph/nodes/text/src/regex.rs @@ -1,6 +1,6 @@ use core_types::list::{Item, List}; use core_types::registry::types::SignedInteger; -use core_types::{ATTR_END, ATTR_NAME, ATTR_START, Ctx}; +use core_types::{Ctx, attr}; /// Checks whether the string contains a match for the given regular expression pattern. Optionally restricts the match to only the start and/or end of the string. #[node_macro::node(category("Text: Regex"))] @@ -159,10 +159,7 @@ fn regex_find( let start = captured.map_or(0_u64, |m| m.start() as u64); let end = captured.map_or(0_u64, |m| m.end() as u64); let name = capture_names.get(i).cloned().flatten().unwrap_or_default(); - Item::new_from_element(text) - .with_attribute(ATTR_START, start) - .with_attribute(ATTR_END, end) - .with_attribute(ATTR_NAME, name) + Item::new_from_element(text).with_attr::(start).with_attr::(end).with_attr::(name) }) .collect() } @@ -208,8 +205,8 @@ fn regex_find_all( .filter_map(|m| m.ok()) .map(|m| { Item::new_from_element(m.as_str().to_string()) - .with_attribute(ATTR_START, m.start() as u64) - .with_attribute(ATTR_END, m.end() as u64) + .with_attr::(m.start() as u64) + .with_attr::(m.end() as u64) }) .collect() } diff --git a/node-graph/nodes/text/src/to_path.rs b/node-graph/nodes/text/src/to_path.rs index 0aca43caa4..9b018ee8f6 100644 --- a/node-graph/nodes/text/src/to_path.rs +++ b/node-graph/nodes/text/src/to_path.rs @@ -1,11 +1,7 @@ use super::TypesettingConfig; use super::text_context::TextContext; -use core_types::blending::BlendMode; -use core_types::list::{Item, List, NodeIdPath}; -use core_types::{ - ATTR_BLEND_MODE, ATTR_EDITOR_LAYER_PATH, ATTR_FONT, ATTR_FONT_SIZE, ATTR_LETTER_SPACING, ATTR_LETTER_TILT, ATTR_LINE_HEIGHT, ATTR_MAX_HEIGHT, ATTR_MAX_WIDTH, ATTR_OPACITY, ATTR_OPACITY_FILL, - ATTR_TEXT_ALIGN, ATTR_TRANSFORM, -}; +use core_types::attr; +use core_types::list::{Item, List}; use glam::{DAffine2, DVec2}; use graphene_resource::Resource; use vector_types::Vector; @@ -33,45 +29,45 @@ pub fn shape_text_item(item: &Item, separate_glyphs: bool) -> List(); if font.is_empty() { super::FALLBACK_FONT_RESOURCE.clone() } else { font } }; let defaults = TypesettingConfig::default(); let typesetting = TypesettingConfig { - font_size: item.attribute_cloned_or(ATTR_FONT_SIZE, defaults.font_size), - line_height_ratio: item.attribute_cloned_or(ATTR_LINE_HEIGHT, defaults.line_height_ratio), - letter_spacing: item.attribute_cloned_or(ATTR_LETTER_SPACING, defaults.letter_spacing), - letter_tilt: item.attribute_cloned_or(ATTR_LETTER_TILT, defaults.letter_tilt), - max_width: item.attribute_cloned_or::>(ATTR_MAX_WIDTH, defaults.max_width), - max_height: item.attribute_cloned_or::>(ATTR_MAX_HEIGHT, defaults.max_height), - align: item.attribute_cloned_or(ATTR_TEXT_ALIGN, defaults.align), + font_size: item.attr_cloned_or::(defaults.font_size), + line_height_ratio: item.attr_cloned_or::(defaults.line_height_ratio), + letter_spacing: item.attr_cloned_or::(defaults.letter_spacing), + letter_tilt: item.attr_cloned_or::(defaults.letter_tilt), + max_width: item.attr_cloned_or::(defaults.max_width), + max_height: item.attr_cloned_or::(defaults.max_height), + align: item.attr_cloned_or::(defaults.align), }; let vectors = to_path(text, &font, typesetting, separate_glyphs); - let transform = item.attribute_cloned_or_default::(ATTR_TRANSFORM); - let layer_path = item.attribute::(ATTR_EDITOR_LAYER_PATH).cloned(); - let blend_mode = item.attribute::(ATTR_BLEND_MODE).copied(); - let opacity = item.attribute::(ATTR_OPACITY).copied(); - let opacity_fill = item.attribute::(ATTR_OPACITY_FILL).copied(); + let transform = item.attr_cloned_or_default::(); + let layer_path = item.attr::().cloned(); + let blend_mode = item.attr::().copied(); + let opacity = item.attr::().copied(); + let opacity_fill = item.attr::().copied(); let mut result = List::new(); for mut produced in vectors.into_iter() { if transform != DAffine2::IDENTITY { - let local = produced.attribute_cloned_or_default::(ATTR_TRANSFORM); - produced.set_attribute(ATTR_TRANSFORM, transform * local); + let local = produced.attr_cloned_or_default::(); + produced.set_attr::(transform * local); } if let Some(layer_path) = &layer_path { - produced.set_attribute(ATTR_EDITOR_LAYER_PATH, layer_path.clone()); + produced.set_attr::(layer_path.clone()); } if let Some(blend_mode) = blend_mode { - produced.set_attribute(ATTR_BLEND_MODE, blend_mode); + produced.set_attr::(blend_mode); } if let Some(opacity) = opacity { - produced.set_attribute(ATTR_OPACITY, opacity); + produced.set_attr::(opacity); } if let Some(opacity_fill) = opacity_fill { - produced.set_attribute(ATTR_OPACITY_FILL, opacity_fill); + produced.set_attr::(opacity_fill); } result.push(produced); } diff --git a/node-graph/nodes/transform/src/transform_nodes.rs b/node-graph/nodes/transform/src/transform_nodes.rs index 2ab2b005c8..a84185de7a 100644 --- a/node-graph/nodes/transform/src/transform_nodes.rs +++ b/node-graph/nodes/transform/src/transform_nodes.rs @@ -1,8 +1,9 @@ use core::f64; +use core_types::attr; use core_types::color::Color; use core_types::list::{Item, List}; use core_types::transform::{ApplyTransform, ScaleType, Transform}; -use core_types::{ATTR_TRANSFORM, CloneVarArgs, Context, Ctx, ExtractAll, InjectFootprint, ModifyFootprint, OwnedContextImpl}; +use core_types::{CloneVarArgs, Context, Ctx, ExtractAll, InjectFootprint, ModifyFootprint, OwnedContextImpl}; use glam::{DAffine2, DMat2, DVec2}; use graphic_types::raster_types::{CPU, GPU, Raster}; use graphic_types::{Artboard, Graphic, Vector}; @@ -106,7 +107,7 @@ fn reset_transform( let mut content = content; let (reset_translation, reset_rotation, reset_scale) = (*reset_translation.element(), *reset_rotation.element(), *reset_scale.element()); - let item_transform = content.attribute_mut_or_insert_default::(ATTR_TRANSFORM); + let item_transform = content.attr_mut_or_insert_default::(); if reset_translation { item_transform.translation = DVec2::ZERO; @@ -147,14 +148,14 @@ fn replace_transform( let mut content = content; let transform = *transform.element(); - content.set_attribute(ATTR_TRANSFORM, transform.transform()); + content.set_attr::(transform.transform()); content } /// Obtains the transform of the input content. #[node_macro::node(category("Math: Transform"), path(core_types::vector))] fn extract_transform(_: impl Ctx, #[implementations(Graphic, Vector, Raster, Raster, Color, Gradient, String, Artboard)] content: Item) -> Item { - Item::new_from_element(content.attribute_cloned_or_default(ATTR_TRANSFORM)) + Item::new_from_element(content.attr_cloned_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_modification_nodes.rs b/node-graph/nodes/vector/src/vector_modification_nodes.rs index 9de8512705..91c2d0b5a9 100644 --- a/node-graph/nodes/vector/src/vector_modification_nodes.rs +++ b/node-graph/nodes/vector/src/vector_modification_nodes.rs @@ -1,7 +1,7 @@ use core_types::list::{Item, List, NodeIdPath}; use core_types::transform::BakeTransform; use core_types::uuid::NodeId; -use core_types::{ATTR_EDITOR_CLICK_TARGET, ATTR_EDITOR_LAYER_PATH, ATTR_TRANSFORM, Ctx}; +use core_types::{Ctx, attr}; use glam::{DAffine2, DVec2}; use graphic_types::Vector; use vector_types::vector::VectorModification; @@ -13,7 +13,7 @@ async fn path_modify(_ctx: impl Ctx, vector: Item, modification: Item(ATTR_EDITOR_CLICK_TARGET); + vector.remove_attr::(); // Set the path to the encapsulating subgraph (drop our own trailing entry from `node_path`), // matching the `path_of_subgraph` proto so editor tools can route data back to the parent layer. @@ -22,9 +22,9 @@ async fn path_modify(_ctx: impl Ctx, vector: Item, modification: Item(ATTR_EDITOR_LAYER_PATH).0; + let existing = vector.attr_cloned_or_default::().0; let layer_path = if existing.is_empty() { subgraph_path } else { existing }; - vector.set_attribute(ATTR_EDITOR_LAYER_PATH, NodeIdPath(layer_path)); + vector.set_attr::(NodeIdPath(layer_path)); vector } @@ -33,7 +33,7 @@ async fn path_modify(_ctx: impl Ctx, vector: Item, modification: Item(_ctx: impl Ctx, #[implementations(Vector, DAffine2, DVec2)] content: Item) -> Item { let mut content = content; - if let Some(transform) = content.remove_attribute::(ATTR_TRANSFORM) { + if let Some(transform) = content.remove_attr::() { content.element_mut().bake_transform(&transform); } diff --git a/node-graph/nodes/vector/src/vector_nodes.rs b/node-graph/nodes/vector/src/vector_nodes.rs index d4cb1fc979..7ee334abb1 100644 --- a/node-graph/nodes/vector/src/vector_nodes.rs +++ b/node-graph/nodes/vector/src/vector_nodes.rs @@ -1,16 +1,14 @@ use core::cmp::Ordering; use core::f64::consts::{PI, TAU}; use core::hash::{Hash, Hasher}; +use core_types::attr::{self, Attr}; use core_types::blending::BlendMode; use core_types::bounds::{BoundingBox, RenderBoundingBox}; -use core_types::list::{ATTR_FILL, ATTR_STROKE, Item, ItemAttributeValues, List, ListDyn, NodeIdPath}; +use core_types::list::{Item, ItemAttributeValues, List, ListDyn}; use core_types::registry::types::{Angle, Length, Multiplier, Percentage, PixelLength, Progression, SeedValue}; use core_types::transform::{Footprint, Transform}; use core_types::uuid::NodeId; -use core_types::{ - ATTR_BLEND_MODE, ATTR_CLIPPING_MASK, ATTR_EDITOR_LAYER_PATH, ATTR_EDITOR_MERGED_LAYERS, ATTR_GRADIENT_TYPE, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_SPREAD_METHOD, ATTR_TRANSFORM, CloneVarArgs, - Color, Context, Ctx, ExtractAll, OwnedContextImpl, -}; +use core_types::{CloneVarArgs, Color, Context, Ctx, ExtractAll, OwnedContextImpl}; use glam::{DAffine2, DMat2, DVec2}; use graphic_types::Vector; use graphic_types::graphic::{bake_paint_transforms, graphic_list_at, has_paint_at, is_paint_present, set_paint_attribute_at}; @@ -73,32 +71,32 @@ impl VectorListIterMut for List { trait VectorItemMut { fn for_each_vector_mut(&mut self, f: impl FnMut(&mut Vector, DAffine2)); - fn set_vector_paint(&mut self, key: &str, paint: List); + fn set_vector_paint>>(&mut self, paint: List); } impl VectorItemMut for Item { fn for_each_vector_mut(&mut self, mut f: impl FnMut(&mut Vector, DAffine2)) { - let transform = self.attribute_cloned_or_default::(ATTR_TRANSFORM); + let transform = self.attr_cloned_or_default::(); f(self.element_mut(), transform); } - fn set_vector_paint(&mut self, key: &str, paint: List) { - self.set_attribute(key, paint); + fn set_vector_paint>>(&mut self, paint: List) { + self.set_attr::(paint); } } impl VectorItemMut for Item { fn for_each_vector_mut(&mut self, mut f: impl FnMut(&mut Vector, DAffine2)) { let Some(vector_list) = self.element_mut().as_vector_mut() else { return }; - let (elements, transforms) = vector_list.element_and_attribute_slices_mut::(ATTR_TRANSFORM); + let (elements, transforms) = vector_list.element_and_attr_slices_mut::(); for (vector, transform) in elements.iter_mut().zip(transforms.iter()) { f(vector, *transform); } } - fn set_vector_paint(&mut self, key: &str, paint: List) { + fn set_vector_paint>>(&mut self, paint: List) { let Some(vector_list) = self.element_mut().as_vector_mut() else { return }; - for slot in vector_list.iter_attribute_values_mut_or_default::>(key) { + for slot in vector_list.iter_attr_values_mut_or_default::() { *slot = paint.clone(); } } @@ -161,10 +159,10 @@ where let paint = List::new_from_element(color).into_graphic_list(); if fill { - set_paint_attribute_at(vector_list, index, ATTR_FILL, paint.clone()); + set_paint_attribute_at::(vector_list, index, paint.clone()); } if stroke && vector_list.element(index).is_some_and(|vector| vector.stroke.is_some()) { - set_paint_attribute_at(vector_list, index, ATTR_STROKE, paint.clone()); + set_paint_attribute_at::(vector_list, index, paint.clone()); } i += 1; @@ -208,19 +206,19 @@ where for graphic in fill.iter_element_values_mut() { let Graphic::Gradient(gradient) = graphic else { continue }; - if gradient.iter_attribute_values::(ATTR_GRADIENT_TYPE).is_none() { - for value in gradient.iter_attribute_values_mut_or_default::(ATTR_GRADIENT_TYPE) { + if gradient.iter_attr_values::().is_none() { + for value in gradient.iter_attr_values_mut_or_default::() { *value = _gradient_type; } } - if gradient.iter_attribute_values::(ATTR_SPREAD_METHOD).is_none() { - for value in gradient.iter_attribute_values_mut_or_default::(ATTR_SPREAD_METHOD) { + if gradient.iter_attr_values::().is_none() { + for value in gradient.iter_attr_values_mut_or_default::() { *value = _spread_method; } } - if gradient.iter_attribute_values::(ATTR_TRANSFORM).is_none() { + if gradient.iter_attr_values::().is_none() { // Without an explicit placement, derive one covering the paint target's bounding box (the CSS `auto` behavior) let transform = if _has_transform { _transform @@ -246,13 +244,13 @@ where initial_gradient_transform_for_bounding_box([min, max]) }; - for value in gradient.iter_attribute_values_mut_or_default::(ATTR_TRANSFORM) { + for value in gradient.iter_attr_values_mut_or_default::() { *value = transform; } } } - content.set_vector_paint(ATTR_FILL, fill); + content.set_vector_paint::(fill); content } @@ -326,7 +324,7 @@ where }); let paint = paint.into_graphic_list(); - content.set_vector_paint(ATTR_STROKE, paint); + content.set_vector_paint::(paint); content } @@ -388,7 +386,7 @@ async fn copy_to_points( let do_scale = random_scale_difference.abs() > 1e-6; let do_rotation = random_rotation.abs() > 1e-6; - let points_transform: DAffine2 = row.attribute_cloned_or_default(ATTR_TRANSFORM); + let points_transform: DAffine2 = row.attr_cloned_or_default::(); for &point in row.element().point_domain.positions() { let translation = points_transform.transform_point2(point); @@ -417,8 +415,8 @@ async fn copy_to_points( for row_index in 0..content.len() { let Some(mut row) = content.clone_item(row_index) else { continue }; - let row_transform: DAffine2 = row.attribute_cloned_or_default(ATTR_TRANSFORM); - row.set_attribute(ATTR_TRANSFORM, transform * row_transform); + let row_transform: DAffine2 = row.attr_cloned_or_default::(); + row.set_attr::(transform * row_transform); result_list.push(row); } @@ -446,7 +444,7 @@ async fn round_corners( min_angle_threshold: Item, ) -> Item { let (radius, roundness, edge_length_limit, min_angle_threshold) = (*radius.element(), *roundness.element(), *edge_length_limit.element(), *min_angle_threshold.element()); - let source_transform: DAffine2 = source.attribute_cloned_or_default(ATTR_TRANSFORM); + let source_transform: DAffine2 = source.attr_cloned_or_default::(); let source_transform_inverse = source_transform.inverse(); let (source, attributes) = source.into_parts(); @@ -550,7 +548,7 @@ pub fn merge_by_distance( match algorithm { MergeByDistanceAlgorithm::Spatial => { - let transform: DAffine2 = content.attribute_cloned_or_default(ATTR_TRANSFORM); + let transform: DAffine2 = content.attr_cloned_or_default::(); content.element_mut().merge_by_distance_spatial(transform, distance); } MergeByDistanceAlgorithm::Topological => content.element_mut().merge_by_distance_topological(distance), @@ -767,12 +765,12 @@ async fn extrude(_: impl Ctx, source: Item, direction: Item, join #[node_macro::node(category("Vector: Modifier"), path(core_types::vector))] async fn box_warp(_: impl Ctx, content: Item, #[expose] rectangle: Item) -> Item { - let target_transform: DAffine2 = rectangle.attribute_cloned_or_default(ATTR_TRANSFORM); + let target_transform: DAffine2 = rectangle.attr_cloned_or_default::(); let target = rectangle.into_element(); let mut row = content; { - let transform: DAffine2 = row.attribute_cloned_or_default(ATTR_TRANSFORM); + let transform: DAffine2 = row.attr_cloned_or_default::(); let vector = std::mem::take(row.element_mut()); // Get the bounding box of the source vector geometry @@ -832,7 +830,7 @@ async fn box_warp(_: impl Ctx, content: Item, #[expose] rectangle: Item< // Reset the transform since we've applied it directly to the points *row.element_mut() = result; - row.set_attribute(ATTR_TRANSFORM, DAffine2::IDENTITY); + row.set_attr::(DAffine2::IDENTITY); } row } @@ -942,8 +940,8 @@ where RowsOrColumns::Rows => DVec2::new(strip.along_position, strip.cross_position), RowsOrColumns::Columns => DVec2::new(strip.cross_position, strip.along_position), }; - let row_transform: DAffine2 = row.attribute_cloned_or_default(ATTR_TRANSFORM); - row.set_attribute(ATTR_TRANSFORM, DAffine2::from_translation(target_position - top_left) * row_transform); + let row_transform: DAffine2 = row.attr_cloned_or_default::(); + row.set_attr::(DAffine2::from_translation(target_position - top_left) * row_transform); strip.along_position += along + separation; } else { @@ -954,8 +952,8 @@ where RowsOrColumns::Rows => DVec2::new(0., new_cross), RowsOrColumns::Columns => DVec2::new(new_cross, 0.), }; - let row_transform: DAffine2 = row.attribute_cloned_or_default(ATTR_TRANSFORM); - row.set_attribute(ATTR_TRANSFORM, DAffine2::from_translation(target_position - top_left) * row_transform); + let row_transform: DAffine2 = row.attr_cloned_or_default::(); + row.set_attr::(DAffine2::from_translation(target_position - top_left) * row_transform); strips.push(Strip { along_position: along + separation, @@ -986,7 +984,7 @@ async fn auto_tangents( ) -> Item { let (spread, preserve_existing) = (*spread.element(), *preserve_existing.element()); - let transform: DAffine2 = source.attribute_cloned_or_default(ATTR_TRANSFORM); + let transform: DAffine2 = source.attr_cloned_or_default::(); let (source, attributes) = source.into_parts(); let mut result = Vector { @@ -1146,7 +1144,7 @@ async fn bounding_box(_: impl Ctx, content: Item) -> Item { async fn dimensions(_: impl Ctx, content: Item) -> Item { let dimensions = content .element() - .bounding_box_with_transform(content.attribute_cloned_or_default(ATTR_TRANSFORM)) + .bounding_box_with_transform(content.attr_cloned_or_default::()) .map(|[top_left, bottom_right]| bottom_right - top_left) .unwrap_or_default(); @@ -1358,7 +1356,7 @@ async fn offset_path(_: impl Ctx, content: Item, distance: Item, jo let mut content = content; let (distance, join, miter_limit) = (*distance.element(), *join.element(), *miter_limit.element()); - let transform_attribute: DAffine2 = content.attribute_cloned_or_default(ATTR_TRANSFORM); + let transform_attribute: DAffine2 = content.attr_cloned_or_default::(); let transform = Affine::new(transform_attribute.to_cols_array()); let vector = std::mem::take(content.element_mut()); @@ -1406,7 +1404,7 @@ where let flattened: List = graphic_list.clone().into_flattened_list(); // A fill exists when the canonical attribute carries paint - let has_fills: Vec = (0..flattened.len()).map(|index| has_paint_at(&flattened, index, ATTR_FILL)).collect(); + let has_fills: Vec = (0..flattened.len()).map(|index| has_paint_at::(&flattened, index)).collect(); let mut output: List = flattened .into_iter() @@ -1466,14 +1464,14 @@ where vector.stroke = None; let mut fill_attributes = attributes.clone(); // No stroke remains on the fill row - fill_attributes.remove::>(ATTR_STROKE); + fill_attributes.remove_attr::(); Item::from_parts(vector, fill_attributes) }); let mut stroke_attributes = attributes; // Drop the original fill and use the stroke paint to fill the outlined stroke - stroke_attributes.remove::>(ATTR_FILL); - stroke_attributes.rename(ATTR_STROKE, ATTR_FILL); + stroke_attributes.remove_attr::(); + stroke_attributes.rename(graphic_types::attr::Stroke::name(), graphic_types::attr::Fill::name()); let stroke_row = Item::from_parts(solidified_stroke, stroke_attributes); @@ -1492,15 +1490,15 @@ where // already holds the original transforms; pre-compensate by row 0's inverse so the renderer's // `upstream_footprint *= row_0_transform` recursion cancels out and leaves the originals intact. let mut graphic_list = graphic_list; - let row_0_transform: DAffine2 = output.attribute_cloned_or_default(ATTR_TRANSFORM, 0); + let row_0_transform: DAffine2 = output.attr_cloned_or_default::(0); if row_0_transform.matrix2.determinant().abs() > f64::EPSILON { let inverse = row_0_transform.inverse(); - for transform in graphic_list.iter_attribute_values_mut_or_default::(ATTR_TRANSFORM) { + for transform in graphic_list.iter_attr_values_mut_or_default::() { *transform = inverse * *transform; } } - output.set_attribute(ATTR_EDITOR_MERGED_LAYERS, 0, graphic_list); + output.set_attr::(0, graphic_list); } output @@ -1574,14 +1572,14 @@ pub async fn combine_paths(_: impl Ctx, #[implementations(Li // Concatenate every vector element's subpaths into the single output compound path for index in 0..flattened.len() { let Some(element) = flattened.element(index) else { continue }; - let layer_path: List = flattened.attribute_cloned_or_default::(ATTR_EDITOR_LAYER_PATH, index).0; + let layer_path: List = flattened.attr_cloned_or_default::(index).0; let node_id = layer_path.iter_element_values().next_back().map(|node_id| node_id.0).unwrap_or_default(); let mut hasher = DefaultHasher::new(); (index, node_id).hash(&mut hasher); let collision_hash_seed = hasher.finish(); - let source_transform = flattened.attribute_cloned_or_default(ATTR_TRANSFORM, index); + let source_transform = flattened.attr_cloned_or_default::(index); output.concat(element, source_transform, collision_hash_seed); // TODO: Make this instead use the first encountered stroke @@ -1595,10 +1593,10 @@ pub async fn combine_paths(_: impl Ctx, #[implementations(Li let source_attributes = flattened.clone_item_attributes(primary); let mut attributes = ItemAttributeValues::new(); - attributes.insert_cloned_from(&source_attributes, ATTR_FILL); - attributes.insert_cloned_from(&source_attributes, ATTR_STROKE); + attributes.insert_cloned_from(&source_attributes, graphic_types::attr::Fill::name()); + attributes.insert_cloned_from(&source_attributes, graphic_types::attr::Stroke::name()); // Adopt the last input item's layer (if any) so the editor can also bucket clicks under a contributing child layer - attributes.insert_cloned_from(&source_attributes, ATTR_EDITOR_LAYER_PATH); + attributes.insert_cloned_from(&source_attributes, attr::editor::LayerPath::name()); bake_paint_transforms(&mut attributes, source_transform); let output = std::mem::take(output_list.element_mut(0).unwrap()); @@ -1608,7 +1606,7 @@ pub async fn combine_paths(_: impl Ctx, #[implementations(Li // Preserve a reference to the original upstream `List` so the renderer can recurse into it // when collecting metadata, exposing the original child layers' click targets to editor tools. // This is the same mechanism Boolean Operation uses to keep its inputs editable after the merge. - output_list.set_attribute(ATTR_EDITOR_MERGED_LAYERS, 0, graphic_list); + output_list.set_attr::(0, graphic_list); output_list.into_iter().next().unwrap_or_default() } @@ -1654,12 +1652,12 @@ async fn sample_polyline( stroke: std::mem::take(&mut content.element_mut().stroke), }; // Transfer the stroke transform from the input vector content to the result. - result.set_stroke_transform(content.attribute_cloned_or_default(ATTR_TRANSFORM)); + result.set_stroke_transform(content.attr_cloned_or_default::()); for local_bezpath in content.element().stroke_bezpath_iter() { // Apply the transform to compute sample locations in world space (for correct distance-based spacing) let mut world_bezpath = local_bezpath.clone(); - let transform_attribute: DAffine2 = content.attribute_cloned_or_default(ATTR_TRANSFORM); + let transform_attribute: DAffine2 = content.attr_cloned_or_default::(); world_bezpath.apply_affine(Affine::new(transform_attribute.to_cols_array())); // Per-segment perimeter lengths (transform-baked) for distance-based spacing @@ -1718,7 +1716,7 @@ async fn simplify( let options = SimplifyOptions::default(); - let transform_attribute: DAffine2 = content.attribute_cloned_or_default(ATTR_TRANSFORM); + let transform_attribute: DAffine2 = content.attr_cloned_or_default::(); let transform = Affine::new(transform_attribute.to_cols_array()); let inverse_transform = transform.inverse(); @@ -1812,7 +1810,7 @@ async fn decimate( points.iter().enumerate().filter(|(i, _)| keep[*i]).map(|(_, p)| *p).collect() } - let transform_attribute: DAffine2 = content.attribute_cloned_or_default(ATTR_TRANSFORM); + let transform_attribute: DAffine2 = content.attr_cloned_or_default::(); let transform = Affine::new(transform_attribute.to_cols_array()); let inverse_transform = transform.inverse(); @@ -1992,7 +1990,7 @@ async fn position_on_path( let (progression, reverse, parameterized_distance) = (progression.into_element(), reverse.into_element(), parameterized_distance.into_element()); let euclidian = !parameterized_distance; - let transform: DAffine2 = content.attribute_cloned_or_default(ATTR_TRANSFORM); + let transform: DAffine2 = content.attr_cloned_or_default::(); let mut bezpaths: Vec<_> = content.element().stroke_bezpath_iter().map(|bezpath| (bezpath, transform)).collect(); let bezpath_count = bezpaths.len() as f64; let progression = progression.clamp(0., bezpath_count); @@ -2031,7 +2029,7 @@ async fn tangent_on_path( let (progression, reverse, parameterized_distance, radians) = (progression.into_element(), reverse.into_element(), parameterized_distance.into_element(), radians.into_element()); let euclidian = !parameterized_distance; - let transform: DAffine2 = content.attribute_cloned_or_default(ATTR_TRANSFORM); + let transform: DAffine2 = content.attr_cloned_or_default::(); let mut bezpaths: Vec<_> = content.element().stroke_bezpath_iter().map(|bezpath| (bezpath, transform)).collect(); let bezpath_count = bezpaths.len() as f64; let progression = progression.clamp(0., bezpath_count); @@ -2222,7 +2220,7 @@ async fn jitter_points( let (max_distance, seed, along_normals) = (*max_distance.element(), *seed.element(), *along_normals.element()); let mut rng = rand::rngs::StdRng::seed_from_u64(seed.into()); - let transform_attribute: DAffine2 = content.attribute_cloned_or_default(ATTR_TRANSFORM); + let transform_attribute: DAffine2 = content.attr_cloned_or_default::(); let inverse_linear = inverse_linear_or_repair(transform_attribute.matrix2); let deltas: Vec<_> = (0..content.element().point_domain.positions().len()) @@ -2247,7 +2245,7 @@ async fn jitter_points( }) .collect(); - let transform: DAffine2 = content.attribute_cloned_or_default(ATTR_TRANSFORM); + let transform: DAffine2 = content.attr_cloned_or_default::(); apply_point_deltas(content.element_mut(), &deltas, transform); content @@ -2267,7 +2265,7 @@ async fn offset_points( ) -> Item { let mut content = content; let distance = *distance.element(); - let transform_attribute: DAffine2 = content.attribute_cloned_or_default(ATTR_TRANSFORM); + let transform_attribute: DAffine2 = content.attr_cloned_or_default::(); let inverse_linear = inverse_linear_or_repair(transform_attribute.matrix2); let deltas: Vec<_> = (0..content.element().point_domain.positions().len()) @@ -2285,7 +2283,7 @@ async fn offset_points( }) .collect(); - let transform: DAffine2 = content.attribute_cloned_or_default(ATTR_TRANSFORM); + let transform: DAffine2 = content.attr_cloned_or_default::(); apply_point_deltas(content.element_mut(), &deltas, transform); content @@ -2409,8 +2407,8 @@ async fn morph( } fn lerp_gradient_transform(gradient_list_a: &List, gradient_list_b: &List, time: f64) -> DAffine2 { - let transform_a = gradient_list_a.attribute_cloned_or_default::(ATTR_TRANSFORM, 0); - let transform_b = gradient_list_b.attribute_cloned_or_default::(ATTR_TRANSFORM, 0); + let transform_a = gradient_list_a.attr_cloned_or_default::(0); + let transform_b = gradient_list_b.attr_cloned_or_default::(0); let start_a = transform_a.translation; let end_a = transform_a.translation + transform_a.matrix2.x_axis; @@ -2470,7 +2468,7 @@ async fn morph( let metadata_source = if time < 0.5 { gradient_list_a } else { gradient_list_b }; let mut gradient_list = metadata_source.clone(); - gradient_list.set_attribute(ATTR_TRANSFORM, 0, lerp_gradient_transform(gradient_list_a, gradient_list_b, time)); + gradient_list.set_attr::(0, lerp_gradient_transform(gradient_list_a, gradient_list_b, time)); gradient_with_stops(gradient_list, stops) }), @@ -2499,7 +2497,7 @@ async fn morph( let default_polyline = || { let mut default_path = BezPath::new(); for index in 0..content.len() { - let transform_attribute: DAffine2 = content.attribute_cloned_or_default(ATTR_TRANSFORM, index); + let transform_attribute: DAffine2 = content.attr_cloned_or_default::(index); let origin = transform_attribute.translation; let point = kurbo::Point::new(origin.x, origin.y); if index == 0 { @@ -2513,7 +2511,7 @@ async fn morph( let control_bezpaths: Vec = { // User-provided path: collect all subpaths with the path's transform applied - let path_transform: DAffine2 = path.attribute_cloned_or_default(ATTR_TRANSFORM); + let path_transform: DAffine2 = path.attr_cloned_or_default::(); let paths: Vec = path .element() .stroke_bezpath_iter() @@ -2585,8 +2583,8 @@ async fn morph( if content.element(source_index).is_none() || content.element(target_index).is_none() { return 0.; } - let source_transform: DAffine2 = content.attribute_cloned_or_default(ATTR_TRANSFORM, source_index); - let target_transform: DAffine2 = content.attribute_cloned_or_default(ATTR_TRANSFORM, target_index); + let source_transform: DAffine2 = content.attr_cloned_or_default::(source_index); + let target_transform: DAffine2 = content.attr_cloned_or_default::(target_index); let (s_angle, s_scale, s_skew) = source_transform.decompose_rotation_scale_skew(); let (t_angle, t_scale, t_skew) = target_transform.decompose_rotation_scale_skew(); @@ -2658,14 +2656,14 @@ async fn morph( }; // Lerp blending attributes: opacity/fill interpolate, blend_mode/clip step at the midpoint - let source_blend_mode: BlendMode = content.attribute_cloned_or_default(ATTR_BLEND_MODE, source_index); - let target_blend_mode: BlendMode = content.attribute_cloned_or_default(ATTR_BLEND_MODE, target_index); - let source_opacity: f64 = content.attribute_cloned_or(ATTR_OPACITY, source_index, 1.); - let target_opacity: f64 = content.attribute_cloned_or(ATTR_OPACITY, target_index, 1.); - let source_fill: f64 = content.attribute_cloned_or(ATTR_OPACITY_FILL, source_index, 1.); - let target_fill: f64 = content.attribute_cloned_or(ATTR_OPACITY_FILL, target_index, 1.); - let source_clip: bool = content.attribute_cloned_or_default(ATTR_CLIPPING_MASK, source_index); - let target_clip: bool = content.attribute_cloned_or_default(ATTR_CLIPPING_MASK, target_index); + let source_blend_mode: BlendMode = content.attr_cloned_or_default::(source_index); + let target_blend_mode: BlendMode = content.attr_cloned_or_default::(target_index); + let source_opacity: f64 = content.attr_cloned_or_default::(source_index); + let target_opacity: f64 = content.attr_cloned_or_default::(target_index); + let source_fill: f64 = content.attr_cloned_or_default::(source_index); + let target_fill: f64 = content.attr_cloned_or_default::(target_index); + let source_clip: bool = content.attr_cloned_or_default::(source_index); + let target_clip: bool = content.attr_cloned_or_default::(target_index); let lerped_blend_mode = if time < 0.5 { source_blend_mode } else { target_blend_mode }; let lerped_opacity = source_opacity + (target_opacity - source_opacity) * time; @@ -2687,8 +2685,8 @@ async fn morph( // This decomposition must match the one used in Stroke::lerp so the renderer's stroke_transform.inverse() // correctly cancels the element transform, keeping the stroke uniform when Stroke is after Transform. let lerped_transform = { - let source_transform: DAffine2 = content.attribute_cloned_or_default(ATTR_TRANSFORM, source_index); - let target_transform: DAffine2 = content.attribute_cloned_or_default(ATTR_TRANSFORM, target_index); + let source_transform: DAffine2 = content.attr_cloned_or_default::(source_index); + let target_transform: DAffine2 = content.attr_cloned_or_default::(target_index); let (s_angle, s_scale, s_skew) = source_transform.decompose_rotation_scale_skew(); let (t_angle, t_scale, t_skew) = target_transform.decompose_rotation_scale_skew(); @@ -2717,7 +2715,7 @@ async fn morph( // in which case we skip pre-compensation to avoid propagating NaN through merged_layers transforms. if lerped_transform.matrix2.determinant().abs() > f64::EPSILON { let lerped_inverse = lerped_transform.inverse(); - for transform in graphic_list_content.iter_attribute_values_mut_or_default::(ATTR_TRANSFORM) { + for transform in graphic_list_content.iter_attr_values_mut_or_default::() { *transform = lerped_inverse * *transform; } } @@ -2729,8 +2727,8 @@ async fn morph( let endpoint_element = content.element(endpoint_index).unwrap(); let mut attributes = content.clone_item_attributes(endpoint_index); - attributes.insert(ATTR_TRANSFORM, lerped_transform); - attributes.insert(ATTR_EDITOR_MERGED_LAYERS, graphic_list_content); + attributes.set_attr::(lerped_transform); + attributes.set_attr::(graphic_list_content); return Item::from_parts(endpoint_element.clone(), attributes); } @@ -2756,13 +2754,13 @@ async fn morph( let mut vector = Vector { stroke, ..Default::default() }; let fill_paint = { - let source = graphic_list_at(&content, source_index, ATTR_FILL); - let target = graphic_list_at(&content, target_index, ATTR_FILL); + let source = graphic_list_at::(&content, source_index); + let target = graphic_list_at::(&content, target_index); lerp_graphic(source.as_deref(), target.as_deref(), time) }; let stroke_paint = { - let source = graphic_list_at(&content, source_index, ATTR_STROKE); - let target = graphic_list_at(&content, target_index, ATTR_STROKE); + let source = graphic_list_at::(&content, source_index); + let target = graphic_list_at::(&content, target_index); lerp_graphic(source.as_deref(), target.as_deref(), time) }; @@ -2913,31 +2911,31 @@ async fn morph( // the click-target identity (so the editor can route clicks back to one of the contributing layers) let primary_index = if time < 0.5 { source_index } else { target_index }; let mut item = Item::new_from_element(vector); - item.set_attribute(ATTR_TRANSFORM, lerped_transform); + item.set_attr::(lerped_transform); // Propagate each blending/layer column only when the input carries it, so attribute presence stays determined by the graph rather than by runtime values - if content.attribute::(ATTR_BLEND_MODE, source_index).is_some() { - item.set_attribute(ATTR_BLEND_MODE, lerped_blend_mode); + if content.attr::(source_index).is_some() { + item.set_attr::(lerped_blend_mode); } - if content.attribute::(ATTR_OPACITY, source_index).is_some() { - item.set_attribute(ATTR_OPACITY, lerped_opacity); + if content.attr::(source_index).is_some() { + item.set_attr::(lerped_opacity); } - if content.attribute::(ATTR_OPACITY_FILL, source_index).is_some() { - item.set_attribute(ATTR_OPACITY_FILL, lerped_fill); + if content.attr::(source_index).is_some() { + item.set_attr::(lerped_fill); } - if content.attribute::(ATTR_CLIPPING_MASK, source_index).is_some() { - item.set_attribute(ATTR_CLIPPING_MASK, lerped_clip); + if content.attr::(source_index).is_some() { + item.set_attr::(lerped_clip); } - if let Some(layer_path) = content.attribute::(ATTR_EDITOR_LAYER_PATH, primary_index) { - item.set_attribute(ATTR_EDITOR_LAYER_PATH, layer_path.clone()); + if let Some(layer_path) = content.attr::(primary_index) { + item.set_attr::(layer_path.clone()); } - item.set_attribute(ATTR_EDITOR_MERGED_LAYERS, graphic_list_content); + item.set_attr::(graphic_list_content); if let Some(fill) = fill_paint { - item.set_attribute(ATTR_FILL, fill); + item.set_attr::(fill); } if let Some(stroke) = stroke_paint { - item.set_attribute(ATTR_STROKE, stroke); + item.set_attr::(stroke); } item @@ -3217,7 +3215,7 @@ fn bevel_algorithm(mut vector: Vector, transform: DAffine2, distance: f64) -> Ve fn bevel(_: impl Ctx, source: Item, #[default(10.)] distance: Item) -> Item { let distance = *distance.element(); - let transform: DAffine2 = source.attribute_cloned_or_default(ATTR_TRANSFORM); + let transform: DAffine2 = source.attr_cloned_or_default::(); let (element, attributes) = source.into_parts(); Item::from_parts(bevel_algorithm(element, transform, distance), attributes) @@ -3233,7 +3231,7 @@ fn close_path(_: impl Ctx, source: Item) -> Item { #[node_macro::node(category("Vector: Measure"), path(core_types::vector))] fn point_inside(_: impl Ctx, source: Item, point: Item) -> Item { let point = point.into_element(); - let transform: DAffine2 = source.attribute_cloned_or_default(ATTR_TRANSFORM); + let transform: DAffine2 = source.attr_cloned_or_default::(); let inside = source.element().check_point_inside_shape(transform, point); Item::new_from_element(inside) @@ -3292,7 +3290,7 @@ async fn index_points( #[node_macro::node(category("Vector: Measure"), path(core_types::vector))] async fn path_length(_: impl Ctx, source: Item) -> Item { - let transform: DAffine2 = source.attribute_cloned_or_default(ATTR_TRANSFORM); + let transform: DAffine2 = source.attr_cloned_or_default::(); let length = source .element() .stroke_bezpath_iter() @@ -3310,7 +3308,7 @@ async fn area(ctx: impl Ctx + CloneVarArgs + ExtractAll, content: impl Node(); let area_scale = transform.matrix2.determinant().abs(); let area = vector.element().stroke_bezpath_iter().map(|subpath| subpath.area() * area_scale).sum::(); @@ -3323,7 +3321,7 @@ async fn centroid(ctx: impl Ctx + CloneVarArgs + ExtractAll, content: impl Node< let new_ctx = OwnedContextImpl::from(ctx).with_footprint(Footprint::default()).into_context(); let vector = content.eval(new_ctx).await; - let transform: DAffine2 = vector.attribute_cloned_or_default(ATTR_TRANSFORM); + let transform: DAffine2 = vector.attr_cloned_or_default::(); let position = element_centroid(vector.element(), transform, centroid_type); Item::new_from_element(position) @@ -3390,7 +3388,7 @@ mod test { fn create_vector_item(bezpath: BezPath, transform: DAffine2) -> Item { let mut row = Vector::default(); row.append_bezpath(bezpath); - Item::new_from_element(row).with_attribute(ATTR_TRANSFORM, transform) + Item::new_from_element(row).with_attr::(transform) } fn item(value: T) -> Item { @@ -3559,7 +3557,7 @@ mod test { // Test a rectangular path with non-zero rotation let square = Vector::from_bezpath(Rect::new(-1., -1., 1., 1.).to_path(DEFAULT_ACCURACY)); let mut square = List::new_from_element(square); - square.with_attribute_mut_or_default(ATTR_TRANSFORM, 0, |t: &mut DAffine2| *t *= DAffine2::from_angle(std::f64::consts::FRAC_PI_4)); + square.with_attr_mut_or_default::(0, |t| *t *= DAffine2::from_angle(std::f64::consts::FRAC_PI_4)); let bounding_box = BoundingBoxNodeMapped { content: FutureWrapperNode(square) }.eval(Footprint::default()).await; let bounding_box = bounding_box.element(0).unwrap(); assert_eq!(bounding_box.region_manipulator_groups().count(), 1); @@ -3692,7 +3690,7 @@ mod test { async fn morph() { let mut rectangles = vector_node_from_bezpath(Rect::new(0., 0., 100., 100.).to_path(DEFAULT_ACCURACY)); let mut second_rectangle = rectangles.clone_item(0).unwrap(); - *second_rectangle.attribute_mut_or_insert_default::(ATTR_TRANSFORM) *= DAffine2::from_translation((-100., -100.).into()); + *second_rectangle.attr_mut_or_insert_default::() *= DAffine2::from_translation((-100., -100.).into()); rectangles.push(second_rectangle); let morphed = super::morph( @@ -3712,7 +3710,7 @@ mod test { vec![DVec2::new(0., 0.), DVec2::new(100., 0.), DVec2::new(100., 100.), DVec2::new(0., 100.)] ); // The interpolated transform carries the midpoint translation (approximate due to arc-length parameterization) - assert!((morphed.attribute_cloned_or_default::(ATTR_TRANSFORM, 0).translation - DVec2::new(-50., -50.)).length() < 1e-3); + assert!((morphed.attr_cloned_or_default::(0).translation - DVec2::new(-50., -50.)).length() < 1e-3); } #[tokio::test] @@ -3724,11 +3722,11 @@ mod test { }; let item_a = Item::new_from_element(rect()) - .with_attribute(ATTR_TRANSFORM, DAffine2::IDENTITY) - .with_attribute(ATTR_FILL, List::new_from_element(Color::RED).into_graphic_list()); + .with_attr::(DAffine2::IDENTITY) + .with_attr::(List::new_from_element(Color::RED).into_graphic_list()); let item_b = Item::new_from_element(rect()) - .with_attribute(ATTR_TRANSFORM, DAffine2::from_translation((-100., -100.).into())) - .with_attribute(ATTR_FILL, List::new_from_element(Color::BLUE).into_graphic_list()); + .with_attr::(DAffine2::from_translation((-100., -100.).into())) + .with_attr::(List::new_from_element(Color::BLUE).into_graphic_list()); let mut content = List::new_from_item(item_a); content.push(item_b); @@ -3744,7 +3742,7 @@ mod test { .await; let morphed = List::new_from_item(morphed); - let fill = graphic_list_at(&morphed, 0, ATTR_FILL).expect("Morph should keep the fill paint at the midpoint"); + let fill = graphic_list_at::(&morphed, 0).expect("Morph should keep the fill paint at the midpoint"); // Interpolated color between red and blue should have >0 value on both R and B let Some(Graphic::Color(colors)) = fill.element(0) else { @@ -3825,7 +3823,7 @@ mod test { source.push(curve.as_path_el()); let transform = DAffine2::from_scale_angle_translation(DVec2::splat(10.), 1., DVec2::new(99., 77.)); - let vector_item = Item::new_from_element(Vector::from_bezpath(source)).with_attribute(ATTR_TRANSFORM, transform); + let vector_item = Item::new_from_element(Vector::from_bezpath(source)).with_attr::(transform); let beveled = super::bevel((), vector_item, Item::new_from_element(2_f64.sqrt() * 100.)); let beveled = beveled.element(); From 4ca061443ef4bde3a4e95a38ec7b027685e72733 Mon Sep 17 00:00:00 2001 From: Timon Date: Sat, 18 Jul 2026 13:09:16 +0000 Subject: [PATCH 3/3] Remove the string-keyed attribute constants and accessor family --- node-graph/libraries/core-types/src/lib.rs | 5 - node-graph/libraries/core-types/src/list.rs | 316 ++++---------------- 2 files changed, 64 insertions(+), 257 deletions(-) diff --git a/node-graph/libraries/core-types/src/lib.rs b/node-graph/libraries/core-types/src/lib.rs index 32b35976ff..cb8ed30890 100644 --- a/node-graph/libraries/core-types/src/lib.rs +++ b/node-graph/libraries/core-types/src/lib.rs @@ -25,11 +25,6 @@ pub use ctor; pub use dyn_any::{StaticTypeSized, WasmNotSend, WasmNotSync}; pub use graphene_hash; pub use graphene_hash::CacheHash; -pub use list::{ - ATTR_BACKGROUND, ATTR_BLEND_MODE, ATTR_CLIP, ATTR_CLIPPING_MASK, ATTR_DIMENSIONS, ATTR_EDITOR_CLICK_TARGET, ATTR_EDITOR_LAYER_PATH, ATTR_EDITOR_MERGED_LAYERS, ATTR_EDITOR_TEXT_FRAME, ATTR_END, - ATTR_FONT, ATTR_FONT_SIZE, ATTR_GRADIENT_TYPE, ATTR_LETTER_SPACING, ATTR_LETTER_TILT, ATTR_LINE_HEIGHT, ATTR_LOCATION, ATTR_MAX_HEIGHT, ATTR_MAX_WIDTH, ATTR_NAME, ATTR_OPACITY, ATTR_OPACITY_FILL, - ATTR_SPREAD_METHOD, ATTR_START, ATTR_TEXT_ALIGN, ATTR_TRANSFORM, ATTR_TYPE, -}; pub use memo::MemoHash; pub use no_std_types::AsU32; pub use no_std_types::blending; diff --git a/node-graph/libraries/core-types/src/list.rs b/node-graph/libraries/core-types/src/list.rs index 1247161238..7bee28d79d 100644 --- a/node-graph/libraries/core-types/src/list.rs +++ b/node-graph/libraries/core-types/src/list.rs @@ -8,79 +8,6 @@ use glam::DAffine2; use graphene_hash::CacheHash; use std::fmt::Debug; -// ================================================= -// Standard attribute keys used across the data flow -// ================================================= - -/// Item's `DAffine2` transformation, composed multiplicatively through nested groups. -pub const ATTR_TRANSFORM: &str = "transform"; -/// Item's `BlendMode`, controlling how it composites with content beneath it. -pub const ATTR_BLEND_MODE: &str = "blend_mode"; -/// Item's opacity multiplier (`f64`, implicit default `1.`). -/// Composed multiplicatively through nested groups. Affects content clipped to the item. -pub const ATTR_OPACITY: &str = "opacity"; -/// Item's fill opacity multiplier (`f64`, implicit default `1.`). -/// Like opacity but does not affect content clipped to the item. -pub const ATTR_OPACITY_FILL: &str = "opacity_fill"; -/// `bool` for whether an item inherits the alpha of the content beneath it (clipping mask). -pub const ATTR_CLIPPING_MASK: &str = "clipping_mask"; -/// `NodeIdPath` path from the root network to the layer node owning this item. -/// Used by editor tools to route clicks/selection back to the originating layer. -pub const ATTR_EDITOR_LAYER_PATH: &str = "editor:layer_path"; -/// `List` snapshot of the upstream content that fed into a destructive merge -/// (Boolean Operation, Rasterize, etc.), so the editor can still surface click targets for -/// the original child layers after their content has been collapsed. -pub const ATTR_EDITOR_MERGED_LAYERS: &str = "editor:merged_layers"; -/// Optional `Vector` that overrides the item's own geometry for click-target generation. -/// Used by the 'Text' node for per-glyph bounding-box rectangles so glyphs are selectable -/// by clicking anywhere within their bounds, not just the filled letterform. -pub const ATTR_EDITOR_CLICK_TARGET: &str = "editor:click_target"; -/// `DAffine2` mapping the unit square `[(0, 0), (1, 1)]` (top-left convention) onto the 'Text' -/// node's text frame in this item's local space. Each item carries the frame relative to its own -/// glyph origin so it survives `Index Elements` filtering. The Text tool reads this to position -/// 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"; -/// `u64` byte offset where a regex match begins ('Regex Find All', 'Regex Capture' text nodes). -pub const ATTR_START: &str = "start"; -/// `u64` byte offset where a regex match ends ('Regex Find All', 'Regex Capture' text nodes). -pub const ATTR_END: &str = "end"; -/// `String` for a regex named-capture-group's name, or empty for unnamed groups ('Regex Capture' text node). -pub const ATTR_NAME: &str = "name"; -/// `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. -pub const ATTR_LOCATION: &str = "location"; -/// Artboard's `DVec2` width and height. -pub const ATTR_DIMENSIONS: &str = "dimensions"; -/// Artboard's `Color` background fill. -pub const ATTR_BACKGROUND: &str = "background"; -/// `bool` for whether an artboard clips content to its bounds. -pub const ATTR_CLIP: &str = "clip"; -/// Gradient's `GradientSpreadMethod` (`Pad`, `Reflect`, or `Repeat`). -pub const ATTR_SPREAD_METHOD: &str = "spread_method"; -/// Gradient's `GradientType` (`Linear` or `Radial`). -pub const ATTR_GRADIENT_TYPE: &str = "gradient_type"; -/// Vector graphics object's filled area paint, of type List where T is any graphic type. -pub const ATTR_FILL: &str = "fill"; -/// Vector graphics object's stroke paint, of type List where T is any graphic type. -pub const ATTR_STROKE: &str = "stroke"; -/// Text item's font size in document-space units (`f64`, implicit default `24.`). -pub const ATTR_FONT_SIZE: &str = "font_size"; -/// Text item's font, as a `Resource` of the loaded font file. -pub const ATTR_FONT: &str = "font"; -/// Text item's line height as a ratio of the font size (`f64`, implicit default `1.2`). -pub const ATTR_LINE_HEIGHT: &str = "line_height"; -/// Text item's extra spacing between letters in document-space units (`f64`, implicit default `0.`). -pub const ATTR_LETTER_SPACING: &str = "letter_spacing"; -/// Text item's maximum line-wrap width in document-space units (`Option`, implicit default `None`). -pub const ATTR_MAX_WIDTH: &str = "max_width"; -/// Text item's maximum block height in document-space units, past which lines are not drawn (`Option`, implicit default `None`). -pub const ATTR_MAX_HEIGHT: &str = "max_height"; -/// Text item's faux-italic letter tilt angle in degrees (`f64`, implicit default `0.`). -pub const ATTR_LETTER_TILT: &str = "letter_tilt"; -/// Text item's `TextAlign` horizontal alignment of lines within the block. -pub const ATTR_TEXT_ALIGN: &str = "text_align"; - // ===================== // TYPE: NodeIdPath // ===================== @@ -505,13 +432,6 @@ impl ListDyn { 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.attributes - .iter() - .find_map(|(k, attribute)| if k == key { attribute.get_any(index)?.downcast_ref::() } else { None }) - } - /// Returns a reference to the attribute value at the given runtime key and item index, downcast to `U`, if present and matching. /// For keys known at compile time use [`Self::attr`]; this variant is for keys only known at runtime (e.g. the attribute nodes). pub fn attribute_dyn(&self, key: &str, index: usize) -> Option<&U> { @@ -522,7 +442,7 @@ impl ListDyn { /// Returns a reference to the value of the typed attribute at the given item index, if present. pub fn attr(&self, index: usize) -> Option<&A::Value> { - self.attribute(A::name(), index) + self.attribute_dyn(A::name(), index) } } @@ -644,11 +564,6 @@ impl ItemAttributeValues { .find_map(|(existing_key, value)| if existing_key == key { (**value).as_any_mut().downcast_mut::() } else { None }) } - /// Gets a mutable reference to the value, inserting a default if it doesn't exist or has the wrong type. - pub fn get_or_insert_default_mut(&mut self, key: &str) -> &mut T { - self.get_or_insert_with_mut(key, T::default) - } - /// Gets a mutable reference to the value, inserting the provided default if it doesn't exist or has the wrong type. pub fn get_or_insert_with_mut(&mut self, key: &str, default: impl FnOnce() -> T) -> &mut T { let needs_insert = match self.0.iter().position(|(existing_key, _)| existing_key == key) { @@ -1061,9 +976,9 @@ impl List { self.attributes.keys() } - // ============================ - // Attribute-oriented iteration - // ============================ + // ================= + // Element iteration + // ================= /// Returns an iterator over shared references to all element values. pub fn iter_element_values(&self) -> std::slice::Iter<'_, T> { @@ -1075,28 +990,6 @@ impl List { self.element.iter_mut() } - /// Returns an iterator over shared references to the values of a typed attribute, or `None` if the attribute doesn't exist or has the wrong type. - pub fn iter_attribute_values(&self, key: &str) -> Option> { - self.attributes.get_attribute_slice::(key).map(|s| s.iter()) - } - - /// Returns an iterator over mutable references to the values of a typed attribute attribute, or `None` if the attribute doesn't exist or has the wrong type. - pub fn iter_attribute_values_mut(&mut self, key: &str) -> Option> { - self.attributes.get_attribute_slice_mut::(key).map(|s| s.iter_mut()) - } - - /// Returns an iterator that yields cloned attribute values for the given key, falling back to `U::default()` for each item if the attribute is missing or has the wrong type. - pub fn iter_attribute_values_or_default(&self, key: &str) -> impl Iterator + '_ { - let slice = self.attributes.get_attribute_slice::(key); - let len = self.element.len(); - (0..len).map(move |i| slice.map_or_else(U::default, |s| s[i].clone())) - } - - /// Returns a mutable iterator over a typed attribute, creating the attribute with default values if it doesn't exist or has the wrong type. - pub fn iter_attribute_values_mut_or_default(&mut self, key: &str) -> std::slice::IterMut<'_, U> { - self.attributes.get_or_create_attribute_slice_mut::(key).iter_mut() - } - // ====================== // Indexed element access // ====================== @@ -1115,26 +1008,12 @@ impl List { // Indexed attribute access // ======================== - /// Returns a shared reference to the attribute value at the given item index and key, if it exists and can be downcast to the requested type. - pub fn attribute(&self, key: &str, index: usize) -> Option<&U> { + /// Returns a shared reference to the attribute value at the given item index and runtime key, if it exists and can be downcast to the requested type. + /// For keys known at compile time use [`Self::attr`]; this variant is for keys only known at runtime (e.g. the attribute nodes). + pub fn attribute_dyn(&self, key: &str, index: usize) -> Option<&U> { self.attributes.get_value(key, index) } - /// Returns a clone of the attribute value at the given item index and key, or `U::default()` if absent or of a different type. - pub fn attribute_cloned_or_default(&self, key: &str, index: usize) -> U { - self.attributes.get_value::(key, index).cloned().unwrap_or_default() - } - - /// Returns a clone of the attribute value at the given item index and key, or the provided default if absent or of a different type. - pub fn attribute_cloned_or(&self, key: &str, index: usize, default: U) -> U { - self.attributes.get_value::(key, index).cloned().unwrap_or(default) - } - - /// Sets the attribute value at the given item index and key, creating the attribute with defaults if it doesn't exist. - pub fn set_attribute(&mut self, key: impl Into, index: usize, value: U) { - self.attributes.set_value(key, index, value); - } - /// Sets a single type-erased attribute value at the given index, creating the attribute from the value's underlying type if it doesn't exist (padded with defaults to match the list's length). /// Falls back to default if the value's type doesn't match an existing attribute. pub fn set_attribute_value_dyn(&mut self, key: impl Into, index: usize, value: AttributeValueDyn) { @@ -1149,17 +1028,6 @@ impl List { } } - /// Removes the entire attribute for the given key, if present. - pub fn remove_attribute(&mut self, key: &str) { - self.attributes.remove_attribute(key); - } - - /// Runs the given closure on a mutable reference to the attribute value at the given item index, - /// creating the attribute with defaults if it doesn't exist, and returns the closure's result. - pub fn with_attribute_mut_or_default R>(&mut self, key: &str, index: usize, f: F) -> R { - f(self.attributes.get_or_insert_default_value::(key, index)) - } - /// Returns a debug-formatted display string for the attribute at the given item index and key. pub fn attribute_display_value(&self, key: &str, index: usize, overrides: fn(&dyn std::any::Any) -> Option) -> Option { self.attributes.display_value(key, index, overrides) @@ -1170,26 +1038,13 @@ impl List { self.attributes.get_any_value(key, index) } - // ==================== - // Split borrow helpers - // ==================== - - /// Returns disjoint mutable references to the element slice and a typed attribute slice, creating the attribute with defaults if it doesn't exist. - /// This enables simultaneous mutable access to elements and a single attribute without borrowing conflicts. - pub fn element_and_attribute_slices_mut(&mut self, key: &str) -> (&mut [T], &mut [U]) { - let Self { element, attributes } = self; - let attribute_position = attributes.find_or_create_attribute::(key); - let attribute = (*attributes.attributes[attribute_position].1).as_any_mut().downcast_mut::>().unwrap(); - (element.as_mut_slice(), &mut attribute.0) - } - // ================== // Typed key variants // ================== /// Returns a shared reference to the value of the typed attribute at the given item index, if present. pub fn attr(&self, index: usize) -> Option<&A::Value> { - self.attribute(A::name(), index) + self.attributes.get_value(A::name(), index) } /// Returns a clone of the value of the typed attribute at the given item index, or the key's default value if absent. @@ -1204,28 +1059,28 @@ impl List { /// Sets the value of the typed attribute at the given item index, creating the attribute with defaults if it doesn't exist. pub fn set_attr(&mut self, index: usize, value: A::Value) { - self.set_attribute(A::name(), index, value); + self.attributes.set_value(A::name(), index, value); } /// Removes the entire typed attribute, if present. pub fn remove_attr(&mut self) { - self.remove_attribute(A::name()); + self.attributes.remove_attribute(A::name()); } /// Runs the given closure on a mutable reference to the value of the typed attribute at the given item index, /// creating the attribute with defaults if it doesn't exist, and returns the closure's result. pub fn with_attr_mut_or_default R>(&mut self, index: usize, f: F) -> R { - self.with_attribute_mut_or_default(A::name(), index, f) + f(self.attributes.get_or_insert_default_value::(A::name(), index)) } /// Returns an iterator over shared references to the values of the typed attribute, or `None` if it doesn't exist. pub fn iter_attr_values(&self) -> Option> { - self.iter_attribute_values(A::name()) + self.attributes.get_attribute_slice::(A::name()).map(|s| s.iter()) } /// Returns an iterator over mutable references to the values of the typed attribute, or `None` if it doesn't exist. pub fn iter_attr_values_mut(&mut self) -> Option> { - self.iter_attribute_values_mut(A::name()) + self.attributes.get_attribute_slice_mut::(A::name()).map(|s| s.iter_mut()) } /// Returns an iterator that yields cloned values of the typed attribute, falling back to the key's default value for each item if the attribute is missing. @@ -1237,12 +1092,16 @@ impl List { /// Returns a mutable iterator over the typed attribute, creating the attribute with defaults if it doesn't exist. pub fn iter_attr_values_mut_or_default(&mut self) -> std::slice::IterMut<'_, A::Value> { - self.iter_attribute_values_mut_or_default(A::name()) + self.attributes.get_or_create_attribute_slice_mut::(A::name()).iter_mut() } /// Returns disjoint mutable references to the element slice and the typed attribute's slice, creating the attribute with defaults if it doesn't exist. + /// This enables simultaneous mutable access to elements and a single attribute without borrowing conflicts. pub fn element_and_attr_slices_mut(&mut self) -> (&mut [T], &mut [A::Value]) { - self.element_and_attribute_slices_mut(A::name()) + let Self { element, attributes } = self; + let attribute_position = attributes.find_or_create_attribute::(A::name()); + let attribute = (*attributes.attributes[attribute_position].1).as_any_mut().downcast_mut::>().unwrap(); + (element.as_mut_slice(), &mut attribute.0) } // ================== @@ -1271,7 +1130,7 @@ impl BoundingBox for List { fn bounding_box(&self, transform: DAffine2, include_stroke: bool) -> RenderBoundingBox { let mut combined_bounds = None; - for (element, item_transform) in self.iter_element_values().zip(self.iter_attribute_values_or_default::(ATTR_TRANSFORM)) { + for (element, item_transform) in self.iter_element_values().zip(self.iter_attr_values_or_default::()) { match element.bounding_box(transform * item_transform, include_stroke) { RenderBoundingBox::None => continue, RenderBoundingBox::Infinite => return RenderBoundingBox::Infinite, @@ -1293,7 +1152,7 @@ impl BoundingBox for List { let mut combined_bounds = None; let mut any_infinite = false; - for (element, item_transform) in self.iter_element_values().zip(self.iter_attribute_values_or_default::(ATTR_TRANSFORM)) { + for (element, item_transform) in self.iter_element_values().zip(self.iter_attr_values_or_default::()) { match element.thumbnail_bounding_box(transform * item_transform, include_stroke) { RenderBoundingBox::None => continue, RenderBoundingBox::Infinite => any_infinite = true, @@ -1365,14 +1224,14 @@ impl PartialEq for List { impl ApplyTransform for List { /// Right-multiplies the modification into each item's transform attribute. fn apply_transform(&mut self, modification: &DAffine2) { - for transform in self.iter_attribute_values_mut_or_default::(ATTR_TRANSFORM) { + for transform in self.iter_attr_values_mut_or_default::() { *transform *= *modification; } } /// Left-multiplies the modification into each item's transform attribute. fn left_apply_transform(&mut self, modification: &DAffine2) { - for transform in self.iter_attribute_values_mut_or_default::(ATTR_TRANSFORM) { + for transform in self.iter_attr_values_mut_or_default::() { *transform = *modification * *transform; } } @@ -1469,55 +1328,9 @@ impl Item { &mut self.attributes } - /// Returns a reference to the attribute value for the given key, if it exists and is of the requested type. - pub fn attribute(&self, key: &str) -> Option<&U> { - self.attributes.get(key) - } - - /// Returns the attribute value for the given key, or the provided default if absent or of a different type. - pub fn attribute_or<'a, U: 'static>(&'a self, key: &str, default: &'a U) -> &'a U { - self.attribute(key).unwrap_or(default) - } - - /// Returns a clone of the attribute value for the given key, or the provided default if absent or of a different type. - pub fn attribute_cloned_or(&self, key: &str, default: U) -> U { - self.attribute(key).cloned().unwrap_or(default) - } - - /// Returns a clone of the attribute value for the given key, or `U`'s default value if absent or of a different type. - pub fn attribute_cloned_or_default(&self, key: &str) -> U { - self.attribute(key).cloned().unwrap_or_default() - } - - /// Returns a mutable reference to the attribute value for the given key, if it exists and is of the requested type. - pub fn attribute_mut(&mut self, key: &str) -> Option<&mut U> { - self.attributes.get_mut(key) - } - - /// Returns a mutable reference to the attribute value for the given key, inserting a default value if absent or of a different type. - pub fn attribute_mut_or_insert_default(&mut self, key: &str) -> &mut U { - self.attributes.get_or_insert_default_mut(key) - } - - /// Sets the attribute value for the given key, replacing any existing entry with the same key. - pub fn set_attribute(&mut self, key: impl Into, value: U) { - self.attributes.insert(key, value); - } - - /// Sets the attribute value for the given key and returns the item, enabling builder-style chaining. - pub fn with_attribute(mut self, key: impl Into, value: U) -> Self { - self.set_attribute(key, value); - self - } - - /// Removes and returns the attribute value for the given key, if it exists and is of the requested type. - pub fn remove_attribute(&mut self, key: &str) -> Option { - self.attributes.remove(key) - } - - // ================== - // Typed key variants - // ================== + // ====================== + // Typed attribute access + // ====================== /// Returns a reference to the value of the typed attribute, if present. pub fn attr(&self) -> Option<&A::Value> { @@ -1587,13 +1400,13 @@ impl From for List { impl ApplyTransform for Item { /// Right-multiplies the modification into the item's transform attribute. fn apply_transform(&mut self, modification: &DAffine2) { - let transform = self.attribute_mut_or_insert_default::(ATTR_TRANSFORM); + let transform = self.attr_mut_or_insert_default::(); *transform *= *modification; } /// Left-multiplies the modification into the item's transform attribute. fn left_apply_transform(&mut self, modification: &DAffine2) { - let transform = self.attribute_mut_or_insert_default::(ATTR_TRANSFORM); + let transform = self.attr_mut_or_insert_default::(); *transform = *modification * *transform; } } @@ -1638,6 +1451,7 @@ impl DoubleEndedIterator for ItemIter { #[cfg(test)] mod tests { use super::*; + use crate::attr; // An item that doesn't set opacity must read as fully opaque even once a sibling introduces the // opacity attribute, otherwise the dense store pads it with f64's `0.` default and it vanishes. @@ -1646,65 +1460,63 @@ mod tests { // Collecting items (the path Boolean Operation takes when merging operands) let mut collected = List::<()>::new(); collected.push(Item::new_from_element(())); - collected.push(Item::new_from_element(()).with_attribute(ATTR_OPACITY, 1_f64)); - assert_eq!(collected.attribute_cloned_or_default::(ATTR_OPACITY, 0), 1.); + collected.push(Item::new_from_element(()).with_attr::(1.)); + assert_eq!(collected.attr::(0), Some(&1.)); // Extending one list with another let mut base = List::<()>::new(); base.push(Item::new_from_element(())); let mut tail = List::<()>::new(); - tail.push(Item::new_from_element(()).with_attribute(ATTR_OPACITY_FILL, 1_f64)); + tail.push(Item::new_from_element(()).with_attr::(1.)); base.extend(tail); - assert_eq!(base.attribute_cloned_or_default::(ATTR_OPACITY_FILL, 0), 1.); + assert_eq!(base.attr::(0), Some(&1.)); // Setting one item's opacity leaves the others opaque, not transparent let mut indexed = List::<()>::new(); indexed.push(Item::new_from_element(())); indexed.push(Item::new_from_element(())); - indexed.set_attribute(ATTR_OPACITY, 1, 0.5_f64); - assert_eq!(indexed.attribute_cloned_or_default::(ATTR_OPACITY, 0), 1.); - assert_eq!(indexed.attribute_cloned_or_default::(ATTR_OPACITY, 1), 0.5); + indexed.set_attr::(1, 0.5); + assert_eq!(indexed.attr::(0), Some(&1.)); + assert_eq!(indexed.attr::(1), Some(&0.5)); - // A non-opacity numeric attribute still falls back to its type default + // A non-opacity numeric attribute still pads with its type default let mut other = List::<()>::new(); other.push(Item::new_from_element(())); - other.push(Item::new_from_element(()).with_attribute(ATTR_START, 5_u64)); - assert_eq!(other.attribute_cloned_or_default::(ATTR_START, 0), 0); + other.push(Item::new_from_element(()).with_attr::(5)); + assert_eq!(other.attr::(0), Some(&0)); } // The typed keys must resolve to the same names as the string constants, and the typed // and string-keyed accessors must hit the same storage. #[test] fn typed_attribute_keys() { - use crate::attr; - - assert_eq!(attr::Transform::name(), ATTR_TRANSFORM); - assert_eq!(attr::BlendMode::name(), ATTR_BLEND_MODE); - assert_eq!(attr::Opacity::name(), ATTR_OPACITY); - assert_eq!(attr::OpacityFill::name(), ATTR_OPACITY_FILL); - assert_eq!(attr::ClippingMask::name(), ATTR_CLIPPING_MASK); - assert_eq!(attr::editor::LayerPath::name(), ATTR_EDITOR_LAYER_PATH); - assert_eq!(attr::editor::TextFrame::name(), ATTR_EDITOR_TEXT_FRAME); - assert_eq!(attr::Start::name(), ATTR_START); - assert_eq!(attr::End::name(), ATTR_END); - assert_eq!(attr::Name::name(), ATTR_NAME); - assert_eq!(attr::Type::name(), ATTR_TYPE); - assert_eq!(attr::Location::name(), ATTR_LOCATION); - assert_eq!(attr::Dimensions::name(), ATTR_DIMENSIONS); - assert_eq!(attr::Background::name(), ATTR_BACKGROUND); - assert_eq!(attr::Clip::name(), ATTR_CLIP); - assert_eq!(attr::FontSize::name(), ATTR_FONT_SIZE); - assert_eq!(attr::LineHeight::name(), ATTR_LINE_HEIGHT); - assert_eq!(attr::LetterSpacing::name(), ATTR_LETTER_SPACING); - assert_eq!(attr::MaxWidth::name(), ATTR_MAX_WIDTH); - assert_eq!(attr::MaxHeight::name(), ATTR_MAX_HEIGHT); - assert_eq!(attr::LetterTilt::name(), ATTR_LETTER_TILT); - - // Typed writes are visible through string reads and vice versa + assert_eq!(attr::Transform::name(), "transform"); + assert_eq!(attr::BlendMode::name(), "blend_mode"); + assert_eq!(attr::Opacity::name(), "opacity"); + assert_eq!(attr::OpacityFill::name(), "opacity_fill"); + assert_eq!(attr::ClippingMask::name(), "clipping_mask"); + assert_eq!(attr::editor::LayerPath::name(), "editor:layer_path"); + assert_eq!(attr::editor::TextFrame::name(), "editor:text_frame"); + assert_eq!(attr::Start::name(), "start"); + assert_eq!(attr::End::name(), "end"); + assert_eq!(attr::Name::name(), "name"); + assert_eq!(attr::Type::name(), "type"); + assert_eq!(attr::Location::name(), "location"); + assert_eq!(attr::Dimensions::name(), "dimensions"); + assert_eq!(attr::Background::name(), "background"); + assert_eq!(attr::Clip::name(), "clip"); + assert_eq!(attr::FontSize::name(), "font_size"); + assert_eq!(attr::LineHeight::name(), "line_height"); + assert_eq!(attr::LetterSpacing::name(), "letter_spacing"); + assert_eq!(attr::MaxWidth::name(), "max_width"); + assert_eq!(attr::MaxHeight::name(), "max_height"); + assert_eq!(attr::LetterTilt::name(), "letter_tilt"); + + // Typed writes are visible through dynamic string reads and vice versa let mut item = Item::new_from_element(()); item.set_attr::(0.5); - assert_eq!(item.attribute::(ATTR_OPACITY), Some(&0.5)); - item.set_attribute(ATTR_START, 5_u64); + assert_eq!(item.attributes().get::("opacity"), Some(&0.5)); + item.attributes_mut().insert("start", 5_u64); assert_eq!(item.attr::(), Some(&5)); // A missing attribute reads as the key's declared default