From e5b1fad6758bfb1dc0408c34f68c677e7e3554e9 Mon Sep 17 00:00:00 2001 From: Dennis Kobert Date: Wed, 26 Aug 2026 14:55:27 +0000 Subject: [PATCH] Read render attributes through census markers instead of string keys --- .../libraries/core-types/src/attribute.rs | 23 ++ node-graph/libraries/core-types/src/lane.rs | 75 +++++ node-graph/libraries/core-types/src/lib.rs | 1 + node-graph/libraries/core-types/src/list.rs | 40 +++ .../libraries/graphic-types/src/markers.rs | 12 + .../libraries/rendering/src/render_ext.rs | 13 +- .../libraries/rendering/src/renderer.rs | 256 +++++++++--------- 7 files changed, 283 insertions(+), 137 deletions(-) create mode 100644 node-graph/libraries/core-types/src/lane.rs diff --git a/node-graph/libraries/core-types/src/attribute.rs b/node-graph/libraries/core-types/src/attribute.rs index 0e8e36c21a..e89d28c10a 100644 --- a/node-graph/libraries/core-types/src/attribute.rs +++ b/node-graph/libraries/core-types/src/attribute.rs @@ -35,6 +35,11 @@ pub trait Attribute: 'static { Default::default() } + /// Borrows the value out of legacy list storage, whose stored form is the + /// owned clone [`Self::read_erased`] produces. `None` where the column is + /// absent or holds another type. + fn from_stored<'a>(stored: &'a dyn std::any::Any) -> Option>; + /// # Safety /// `ptr` must point at a live field of this marker's value type. unsafe fn read_erased(ptr: *const u8) -> Box; @@ -178,6 +183,12 @@ macro_rules! attribute { const NAME: &'static str = $name; type Value<'e> = ::core::option::Option<&'e $value>; + fn from_stored<'a>(stored: &'a dyn ::std::any::Any) -> ::core::option::Option> { + stored + .downcast_ref::<::core::option::Option<<$value as ::std::borrow::ToOwned>::Owned>>() + .map(|owned| owned.as_ref().map(::std::borrow::Borrow::borrow)) + } + unsafe fn read_erased(ptr: *const u8) -> ::std::boxed::Box { ::std::boxed::Box::new(unsafe { ptr.cast::<::core::option::Option<&$value>>().read() }.map(|value| <$value as ::std::borrow::ToOwned>::to_owned(value))) } @@ -216,6 +227,10 @@ macro_rules! attribute { } )? + fn from_stored<'a>(stored: &'a dyn ::std::any::Any) -> ::core::option::Option> { + stored.downcast_ref::<<$value as ::std::borrow::ToOwned>::Owned>().map(::std::borrow::Borrow::borrow) + } + unsafe fn read_erased(ptr: *const u8) -> ::std::boxed::Box { ::std::boxed::Box::new(unsafe { ptr.cast::<&$value>().read() }.to_owned()) } @@ -247,6 +262,10 @@ macro_rules! attribute { } )? + fn from_stored<'a>(stored: &'a dyn ::std::any::Any) -> ::core::option::Option> { + stored.downcast_ref::<$value>().copied() + } + unsafe fn read_erased(ptr: *const u8) -> ::std::boxed::Box { ::std::boxed::Box::new(unsafe { ptr.cast::<$value>().read() }) } @@ -373,6 +392,10 @@ mod tests { const NAME: &'static str = "opacity"; type Value<'e> = bool; + fn from_stored<'a>(stored: &'a dyn std::any::Any) -> Option> { + stored.downcast_ref::().copied() + } + unsafe fn read_erased(ptr: *const u8) -> Box { Box::new(unsafe { ptr.cast::().read() }) } diff --git a/node-graph/libraries/core-types/src/lane.rs b/node-graph/libraries/core-types/src/lane.rs new file mode 100644 index 0000000000..5c220db367 --- /dev/null +++ b/node-graph/libraries/core-types/src/lane.rs @@ -0,0 +1,75 @@ +//! The read surface over a source of lanes, so readers name census markers +//! instead of a storage shape. + +use crate::attribute::Attribute; + +/// One marker's column on a source, resolved once so lane reads skip the key +/// lookup. +pub trait LaneColumn<'a, A: Attribute> { + /// The lane's value, or the marker's census default where the column is + /// absent. + fn get(&self, lane: usize) -> A::Value<'a>; +} + +/// A source of lanes carrying an element and census attributes. +pub trait LaneSource { + type Element; + type Column<'a, A: Attribute>: LaneColumn<'a, A> + where + Self: 'a; + + fn lane_count(&self) -> usize; + + fn element(&self, lane: usize) -> Option<&Self::Element>; + + fn column(&self) -> Self::Column<'_, A>; + + fn attr(&self, lane: usize) -> A::Value<'_> { + self.column::().get(lane) + } +} + +#[cfg(test)] +mod tests { + use super::LaneSource; + use crate::attribute::{EditorLayerPath, Opacity, Transform}; + use crate::list::List; + use crate::uuid::NodeId; + use glam::DAffine2; + + #[test] + fn a_plain_marker_reads_what_the_legacy_column_stores() { + let mut list = List::new_from_element(1u32); + let transform = DAffine2::from_translation((3., 4.).into()); + list.set_attribute(crate::ATTR_TRANSFORM, 0, transform); + + assert_eq!(list.attr::(0), transform); + } + + #[test] + fn an_absent_marker_reads_its_census_default_not_the_value_default() { + let list = List::new_from_element(1u32); + + // `Opacity` declares `= 1.`, so the census default must win over `f64::default()`. + assert_eq!(list.attr::(0), 1.); + assert_eq!(list.attr::(0), DAffine2::IDENTITY); + } + + #[test] + fn a_reference_marker_borrows_the_stored_owned_form() { + let mut list = List::new_from_element(1u32); + let path = vec![NodeId(7), NodeId(9)]; + list.set_attribute(crate::ATTR_EDITOR_LAYER_PATH, 0, path.clone()); + + assert_eq!(list.attr::(0), path.as_slice()); + assert!(List::new_from_element(1u32).attr::(0).is_empty()); + } + + #[test] + fn a_column_of_the_wrong_stored_type_reads_as_absent() { + let mut list = List::new_from_element(1u32); + list.set_attribute(crate::ATTR_OPACITY, 0, "not an f64".to_string()); + + assert_eq!(list.attr::(0), 1.); + } +} diff --git a/node-graph/libraries/core-types/src/lib.rs b/node-graph/libraries/core-types/src/lib.rs index 93de249f31..4ec773751a 100644 --- a/node-graph/libraries/core-types/src/lib.rs +++ b/node-graph/libraries/core-types/src/lib.rs @@ -8,6 +8,7 @@ pub mod context; pub mod extent; pub mod frame_table; pub mod gpoll; +pub mod lane; pub mod list; pub mod math; pub mod memo; diff --git a/node-graph/libraries/core-types/src/list.rs b/node-graph/libraries/core-types/src/list.rs index 69fb62b9c9..43ff7467d3 100644 --- a/node-graph/libraries/core-types/src/list.rs +++ b/node-graph/libraries/core-types/src/list.rs @@ -738,6 +738,11 @@ impl Attributes { .find_map(|(k, attribute)| if k == key { attribute.get_any(index)?.downcast_ref::() } else { None }) } + /// The whole column for the given key, resolved once for repeated lane reads. + fn column(&self, key: &str) -> Option<&dyn AnyAttribute> { + self.attributes.iter().find_map(|(k, attribute)| (k == key).then_some(&**attribute)) + } + /// Removes the entire attribute for the given key, if present. fn remove_attribute(&mut self, key: &str) { if let Some(position) = self.attributes.iter().position(|(k, _)| k == key) { @@ -1168,6 +1173,41 @@ impl Default for List { } } +/// A marker's column on a [`List`], absent when the list carries no such key. +pub struct ListColumn<'a, A: crate::attribute::Attribute> { + stored: Option<&'a dyn AnyAttribute>, + marker: std::marker::PhantomData, +} + +impl<'a, A: crate::attribute::Attribute> crate::lane::LaneColumn<'a, A> for ListColumn<'a, A> { + fn get(&self, lane: usize) -> A::Value<'a> { + self.stored.and_then(|column| column.get_any(lane)).and_then(A::from_stored).unwrap_or_else(A::default) + } +} + +impl crate::lane::LaneSource for List { + type Element = T; + type Column<'a, A: crate::attribute::Attribute> + = ListColumn<'a, A> + where + Self: 'a; + + fn lane_count(&self) -> usize { + List::len(self) + } + + fn element(&self, lane: usize) -> Option<&T> { + List::element(self, lane) + } + + fn column(&self) -> ListColumn<'_, A> { + ListColumn { + stored: self.attributes.column(A::NAME), + marker: std::marker::PhantomData, + } + } +} + impl CacheHash for List { fn cache_hash(&self, state: &mut H) { self.element.cache_hash(state); diff --git a/node-graph/libraries/graphic-types/src/markers.rs b/node-graph/libraries/graphic-types/src/markers.rs index 1682af6c43..102da9c250 100644 --- a/node-graph/libraries/graphic-types/src/markers.rs +++ b/node-graph/libraries/graphic-types/src/markers.rs @@ -42,4 +42,16 @@ mod tests { fn an_absent_paint_defaults_to_none() { assert_eq!(::default(), None); } + + #[test] + fn a_paint_marker_reads_back_what_the_paint_writer_stored() { + use core_types::lane::LaneSource; + + let paint = List::new_from_element(Graphic::default()); + let mut list = List::new_from_element(Graphic::default()); + crate::graphic::set_paint_attribute_at(&mut list, 0, ATTR_FILL, paint.clone()); + + assert_eq!(list.attr::(0), Some(&paint)); + assert_eq!(list.attr::(0), None); + } } diff --git a/node-graph/libraries/rendering/src/render_ext.rs b/node-graph/libraries/rendering/src/render_ext.rs index f2df687c53..cb10ed88c6 100644 --- a/node-graph/libraries/rendering/src/render_ext.rs +++ b/node-graph/libraries/rendering/src/render_ext.rs @@ -1,16 +1,19 @@ use crate::renderer::{RenderParams, format_transform_matrix, gradient_placement, transform_is_invertible}; use crate::{Render, RenderSvgSegmentList, SvgRender}; +use core_types::Color; +use core_types::attribute::Transform; use core_types::color::SRGBA8; +use core_types::lane::LaneSource; use core_types::list::List; use core_types::uuid::generate_uuid; -use core_types::{ATTR_TRANSFORM, Color}; use glam::{DAffine2, DVec2}; use graphic_types::Graphic; use graphic_types::vector_types::gradient::GradientType; +use graphic_types::vector_types::markers::{GradientType as GradientTypeAttr, SpreadMethod}; use graphic_types::vector_types::vector::style::{PaintOrder, Stroke, StrokeAlign, StrokeCap, StrokeJoin}; use std::fmt::Write; +use vector_types::GradientStops; use vector_types::gradient::GradientSpreadMethod; -use vector_types::{ATTR_GRADIENT_TYPE, ATTR_SPREAD_METHOD, GradientStops}; #[derive(Copy, Clone, PartialEq)] pub enum PaintTarget { @@ -93,9 +96,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: GradientType = self.attr::(0); + let local_gradient_transform: DAffine2 = self.attr::(0); + let spread_method: GradientSpreadMethod = self.attr::(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: GradientType = gradient_list.attr::(0); + let gradient_transform: DAffine2 = gradient_list.attr::(0); + let spread_method: GradientSpreadMethod = gradient_list.attr::(0); let mut peniko_stops = peniko::ColorStops::new(); for (position, color, _) in stops.interpolated_samples() { @@ -584,9 +587,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: Vec = list.attribute_cloned_or_default(ATTR_EDITOR_LAYER_PATH, 0); + let layer_path: &[NodeId] = list.attr::(0); let layer = layer_path.last().copied(); - let transform: DAffine2 = list.attribute_cloned_or_default(ATTR_TRANSFORM, 0); + let transform: DAffine2 = list.attr::(0); metadata.first_element_source_id.insert(element_id, layer); metadata.local_transforms.insert(element_id, transform); @@ -597,7 +600,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::(0)); } } Graphic::RasterGPU(list) => { @@ -605,7 +608,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::(0)); } } Graphic::Color(list) => { @@ -613,7 +616,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::(0)); } } Graphic::Gradient(list) => { @@ -621,7 +624,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::(0)); } } Graphic::Text(list) => { @@ -629,7 +632,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::(0)); } } } @@ -702,10 +705,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: DVec2 = list.attr::(index); + let dimensions: DVec2 = list.attr::(index); + let background: Color = list.attr::(index); + let clip: bool = list.attr::(index); (location, dimensions, background, clip) } @@ -803,7 +806,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: Vec = self.attribute_cloned_or_default(ATTR_EDITOR_LAYER_PATH, index); + let layer_path: &[NodeId] = self.attr::(index); let element_id = layer_path.last().copied(); if let Some(element_id) = element_id { @@ -826,7 +829,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: DVec2 = self.attr::(index); let subpath_rectangle = Subpath::new_rectangle(DVec2::ZERO, dimensions); click_targets.push(ClickTarget::new_with_subpath(subpath_rectangle, 0.)); } @@ -842,10 +845,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: DAffine2 = self.attr::(index); + let blend_mode: BlendMode = self.attr::(index); + let opacity_attr: f64 = self.attr::(index); + let opacity_fill_attr: f64 = self.attr::(index); let element = self.element(index).unwrap(); render.parent_tag( @@ -898,11 +901,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: DAffine2 = self.attr::(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: BlendMode = self.attr::(index); + let opacity_attr: f64 = self.attr::(index); + let opacity_fill_attr: f64 = self.attr::(index); let element = self.element(index).unwrap(); let mut layer = false; @@ -974,8 +977,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: Vec = self.attribute_cloned_or_default(ATTR_EDITOR_LAYER_PATH, index); + let item_transform: DAffine2 = self.attr::(index); + let layer_path: &[NodeId] = self.attr::(index); let layer = layer_path.last().copied(); let element = self.element(index).unwrap(); @@ -995,7 +998,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: DAffine2 = self.attr::(index); let element = self.element(index).unwrap(); let mut new_click_targets = Vec::new(); @@ -1022,7 +1025,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: DAffine2 = self.attr::(index); let element = self.element(index).unwrap(); let mut new_click_targets = Vec::new(); @@ -1038,7 +1041,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: DAffine2 = self.attr::(index); let element = self.element(index).unwrap(); let mut new_outlines = Vec::new(); @@ -1068,10 +1071,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: DAffine2 = self.attr::(index); + let blend_mode_attr: BlendMode = self.attr::(index); + let opacity_attr: f64 = self.attr::(index); + let opacity_fill_attr: f64 = self.attr::(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.); @@ -1284,10 +1287,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: DAffine2 = self.attr::(index); + let blend_mode_attr: BlendMode = self.attr::(index); + let opacity_attr: f64 = self.attr::(index); + let opacity_fill_attr: f64 = self.attr::(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)); @@ -1559,11 +1562,7 @@ impl Render for List { fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, caller_element_id: Option) { // Aggregate all items' targets per element_id so multi-item lists (e.g. 'Text' node with "Separate Glyphs" active) 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) - } else { - DAffine2::IDENTITY - }; + let item_zero_transform: DAffine2 = if !self.is_empty() { self.attr::(0) } else { DAffine2::IDENTITY }; let item_zero_inverse = if transform_is_invertible(item_zero_transform) { item_zero_transform.inverse() } else { @@ -1575,8 +1574,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: Vec = self.attribute_cloned_or_default(ATTR_EDITOR_LAYER_PATH, index); + let transform: DAffine2 = self.attr::(index); + let layer_path: &[NodeId] = self.attr::(index); let layer = layer_path.last().copied(); if let Some(element_id) = caller_element_id.or(layer) { @@ -1626,8 +1625,7 @@ impl Render for List { // If this item carries a snapshot of upstream graphic content (e.g. it was produced by Boolean Operation, // Flatten Path, 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).unwrap_or_default(); - if !upstream_nested_layers.is_empty() { + if let Some(upstream_nested_layers) = self.attr::(index).filter(|layers| !layers.is_empty()) { let mut upstream_footprint = footprint; upstream_footprint.transform *= transform; upstream_nested_layers.collect_metadata(metadata, upstream_footprint, None); @@ -1646,7 +1644,7 @@ 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: DAffine2 = self.attr::(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); @@ -1659,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: DAffine2 = self.attr::(index); extend_targets_from_vector(outlines, self, index, source, transform); } @@ -1731,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: DAffine2 = self.attr::(index); + let blend_mode_attr: BlendMode = self.attr::(index); + let opacity_attr: f64 = self.attr::(index); + let opacity_fill_attr: f64 = self.attr::(index); if image.data.is_empty() { continue; @@ -1818,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: BlendMode = self.attr::(index); + let opacity_attr: f64 = self.attr::(index); + let opacity_fill_attr: f64 = self.attr::(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; @@ -1835,7 +1833,7 @@ impl Render for List> { layer = true; } - let transform_attribute: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index); + let transform_attribute: DAffine2 = self.attr::(index); if let RenderMode::Outline = render_params.render_mode { let outline_transform: DAffine2 = transform * transform_attribute; @@ -1875,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: DAffine2 = self.attr::(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, @@ -1884,8 +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).unwrap_or_default(); - if !upstream_nested_layers.is_empty() { + if let Some(upstream_nested_layers) = self.attr::(0).filter(|layers| !layers.is_empty()) { upstream_nested_layers.collect_metadata(metadata, footprint, None); } } @@ -1907,10 +1904,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: BlendMode = self.attr::(index); + let opacity_attr: f64 = self.attr::(index); + let opacity_fill_attr: f64 = self.attr::(index); + let clip_attr: bool = self.attr::(index); let blend_mode = match render_params.render_mode { RenderMode::Outline => peniko::Mix::Normal, _ => blend_mode_attr.to_peniko(), @@ -1929,7 +1926,7 @@ impl Render for List> { layer = true; } - let transform_attribute: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index); + let transform_attribute: DAffine2 = self.attr::(index); if let RenderMode::Outline = render_params.render_mode { let outline_transform = transform * transform_attribute; @@ -1970,7 +1967,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: DAffine2 = self.attr::(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, @@ -1979,8 +1976,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).unwrap_or_default(); - if !upstream_nested_layers.is_empty() { + if let Some(upstream_nested_layers) = self.attr::(0).filter(|layers| !layers.is_empty()) { upstream_nested_layers.collect_metadata(metadata, footprint, None); } } @@ -2001,9 +1997,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: BlendMode = self.attr::(index); + let opacity_attr: f64 = self.attr::(index); + let opacity_fill_attr: f64 = self.attr::(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. @@ -2032,9 +2028,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: BlendMode = self.attr::(index); + let opacity_attr: f64 = self.attr::(index); + let opacity_fill_attr: f64 = self.attr::(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; @@ -2072,12 +2068,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: DAffine2 = self.attr::(index); + let blend_mode: BlendMode = self.attr::(index); + let opacity_attr: f64 = self.attr::(index); + let opacity_fill_attr: f64 = self.attr::(index); + let spread_method: GradientSpreadMethod = self.attr::(index); + let gradient_type: GradientType = self.attr::(index); let tag = if thumbnail_rect.is_some() { "rect" } else { "polyline" }; render.leaf_tag(tag, |attributes| { if let Some((min, size)) = thumbnail_rect { @@ -2164,10 +2160,10 @@ impl Render for List { .zip(self.iter_attribute_values_or_default::(ATTR_SPREAD_METHOD)) .zip(self.iter_attribute_values_or_default::(ATTR_GRADIENT_TYPE)) { - 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: DAffine2 = self.attr::(index); + let blend_mode_attr: BlendMode = self.attr::(index); + let opacity_attr: f64 = self.attr::(index); + let opacity_fill_attr: f64 = self.attr::(index); let gradient_transform = parent_transform * transform; let blend_mode = blend_mode_attr.to_peniko(); @@ -2315,16 +2311,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); - if f.is_empty() { text_nodes::FALLBACK_FONT_RESOURCE.clone() } else { f } + let f = list.attr::(index); + if f.is_empty() { text_nodes::FALLBACK_FONT_RESOURCE.clone() } else { f.clone() } }; - 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: f64 = list.attr::(index); + let line_height: f64 = list.attr::(index); + let letter_spacing: f64 = list.attr::(index); + let max_width: Option = list.attr::(index); + let max_height: Option = list.attr::(index); + let align: text_nodes::TextAlign = list.attr::(index); + let transform: DAffine2 = list.attr::(index); let typesetting = text_nodes::TypesettingConfig { font_size, @@ -2376,7 +2372,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::(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), @@ -2410,21 +2406,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: DAffine2 = self.attr::(index); + let opacity_attr: f64 = self.attr::(index); + let opacity_fill_attr: f64 = self.attr::(index); + let blend_mode_attr: BlendMode = self.attr::(index); let font: Resource = { - let f: Resource = self.attribute_cloned_or_default(ATTR_FONT, index); - if f.is_empty() { text_nodes::FALLBACK_FONT_RESOURCE.clone() } else { f } + let f = self.attr::(index); + if f.is_empty() { text_nodes::FALLBACK_FONT_RESOURCE.clone() } else { f.clone() } }; - 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: f64 = self.attr::(index); + let line_height: f64 = self.attr::(index); + let letter_spacing: f64 = self.attr::(index); + let max_width: Option = self.attr::(index); + let max_height: Option = self.attr::(index); + let letter_tilt: f64 = self.attr::(index); + let align: text_nodes::TextAlign = self.attr::(index); let opacity = (opacity_attr * if render_params.for_mask { 1. } else { opacity_fill_attr }) as f32; let typesetting = text_nodes::TypesettingConfig { @@ -2495,21 +2491,21 @@ impl Render for List { continue; } - let item_transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index); + let item_transform: DAffine2 = self.attr::(index); let font: Resource = { - let f: Resource = self.attribute_cloned_or_default(ATTR_FONT, index); - if f.is_empty() { text_nodes::FALLBACK_FONT_RESOURCE.clone() } else { f } + let f = self.attr::(index); + if f.is_empty() { text_nodes::FALLBACK_FONT_RESOURCE.clone() } else { f.clone() } }; - 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: f64 = self.attr::(index); + let line_height: f64 = self.attr::(index); + let letter_spacing: f64 = self.attr::(index); + let max_width: Option = self.attr::(index); + let max_height: Option = self.attr::(index); + let letter_tilt: f64 = self.attr::(index); + let align: text_nodes::TextAlign = self.attr::(index); + let blend_mode_attr: BlendMode = self.attr::(index); + let opacity_attr: f64 = self.attr::(index); + let opacity_fill_attr: f64 = self.attr::(index); let opacity = (opacity_attr * if render_params.for_mask { 1. } else { opacity_fill_attr }) as f32; let typesetting = text_nodes::TypesettingConfig { @@ -2559,11 +2555,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) - } else { - DAffine2::IDENTITY - }; + let item_zero_transform: DAffine2 = if !self.is_empty() { self.attr::(0) } else { DAffine2::IDENTITY }; let item_zero_inverse = if item_zero_transform.matrix2.determinant() != 0. { item_zero_transform.inverse() } else { @@ -2573,7 +2565,7 @@ impl Render for List { let mut accumulated_click_targets: HashMap>> = HashMap::new(); for index in 0..self.len() { - let layer_path: Vec = self.attribute_cloned_or_default(ATTR_EDITOR_LAYER_PATH, index); + let layer_path: &[NodeId] = self.attr::(index); let layer = layer_path.last().copied(); let Some(element_id) = caller_element_id.or(layer) else { continue };