From eb11eec5bbf3c9c8a76577649564979d9c82beae Mon Sep 17 00:00:00 2001 From: Dennis Kobert Date: Wed, 26 Aug 2026 14:02:01 +0000 Subject: [PATCH] Store paint attributes in the paint markers' owned optional form --- .../libraries/graphic-types/src/graphic.rs | 20 +++------ .../libraries/rendering/src/renderer.rs | 6 +-- node-graph/nodes/graphic/src/graphic.rs | 6 +-- .../nodes/gstd/src/platform_application_io.rs | 2 +- node-graph/nodes/vector/src/vector_nodes.rs | 45 +++++++++++++------ 5 files changed, 45 insertions(+), 34 deletions(-) diff --git a/node-graph/libraries/graphic-types/src/graphic.rs b/node-graph/libraries/graphic-types/src/graphic.rs index 21b4b054e3..9cca998729 100644 --- a/node-graph/libraries/graphic-types/src/graphic.rs +++ b/node-graph/libraries/graphic-types/src/graphic.rs @@ -206,13 +206,9 @@ pub fn graphic_list_at<'a>(list: &'a List, index: usize, attribute: &str .filter(|graphic_list| is_paint_present(graphic_list)) } -/// The paint attribute's list, in either transitional storage form: the -/// canonical `List` value, or the fill marker's owned -/// `Option>` clone as the render bridge copies it. +/// The paint attribute's list. Storage is the paint marker's owned +/// `Option>` form, which every writer produces. fn paint_at<'a, T>(list: &'a List, index: usize, attribute: &str) -> Option<&'a List> { - if let Some(graphic_list) = list.attribute::>(attribute, index) { - return Some(graphic_list); - } list.attribute::>>(attribute, index).and_then(|optional| optional.as_ref()) } @@ -222,14 +218,14 @@ pub fn has_paint_at(list: &List, index: usize, attribute: &str) -> bool paint_at(list, index, attribute).is_some_and(is_paint_present) } -/// Stores a paint attribute in its canonical `List` form, the only representation paint readers accept. +/// Stores a paint attribute in the paint marker's owned 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()); + attributes.insert(key, Some(paint.into_graphic_list())); } -/// Stores a paint attribute at a list index in its canonical `List` form, the only representation paint readers accept. +/// Stores a paint attribute at a list index in the paint marker's owned 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()); + list.set_attribute(key, index, Some(paint.into_graphic_list())); } /// Bake the provided transform into the per-item transforms of the paint graphics stored under the @@ -256,9 +252,7 @@ pub fn bake_paint_transforms(attributes: &mut ItemAttributeValues, transform: DA } for paint_key in [ATTR_FILL, ATTR_STROKE] { - if let Some(graphics) = attributes.get_mut::>(paint_key) { - bake_graphic_paint_transform(graphics, transform); - } else if let Some(Some(graphics)) = attributes.get_mut::>>(paint_key) { + if let Some(Some(graphics)) = attributes.get_mut::>>(paint_key) { bake_graphic_paint_transform(graphics, transform); } } diff --git a/node-graph/libraries/rendering/src/renderer.rs b/node-graph/libraries/rendering/src/renderer.rs index af2bbfd471..e3f93546a9 100644 --- a/node-graph/libraries/rendering/src/renderer.rs +++ b/node-graph/libraries/rendering/src/renderer.rs @@ -1626,7 +1626,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); + let upstream_nested_layers = self.attribute_cloned_or_default::>>(ATTR_EDITOR_MERGED_LAYERS, index).unwrap_or_default(); if !upstream_nested_layers.is_empty() { let mut upstream_footprint = footprint; upstream_footprint.transform *= transform; @@ -1884,7 +1884,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.attribute_cloned_or_default::>>(ATTR_EDITOR_MERGED_LAYERS, 0).unwrap_or_default(); if !upstream_nested_layers.is_empty() { upstream_nested_layers.collect_metadata(metadata, footprint, None); } @@ -1979,7 +1979,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.attribute_cloned_or_default::>>(ATTR_EDITOR_MERGED_LAYERS, 0).unwrap_or_default(); if !upstream_nested_layers.is_empty() { upstream_nested_layers.collect_metadata(metadata, footprint, None); } diff --git a/node-graph/nodes/graphic/src/graphic.rs b/node-graph/nodes/graphic/src/graphic.rs index 389fb59ae8..7ecc0c8b3a 100644 --- a/node-graph/nodes/graphic/src/graphic.rs +++ b/node-graph/nodes/graphic/src/graphic.rs @@ -224,8 +224,8 @@ where if mirrored { transform = reflected_transform.expect("a mirrored lane exists only under a reflection") * transform; } - let fill = park_paint(legacy.attribute::>(graphic_types::ATTR_FILL, source).cloned())?; - let stroke = park_paint(legacy.attribute::>(graphic_types::ATTR_STROKE, source).cloned())?; + let fill = park_paint(legacy.attribute::>>(graphic_types::ATTR_FILL, source).cloned().flatten())?; + let stroke = park_paint(legacy.attribute::>>(graphic_types::ATTR_STROKE, source).cloned().flatten())?; let layer_path: Vec = legacy.attribute::>(ATTR_EDITOR_LAYER_PATH, source).map(|path| path.clone()).unwrap_or_default(); let layer_path = arena.alloc(layer_path).ok_or_else(exhausted)?.0; @@ -624,7 +624,7 @@ pub fn flatten_vector(_: impl Ctx, #[implementations(List) -> List { 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_STROKE); 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.remove::>>(ATTR_FILL); stroke_attributes.rename(ATTR_STROKE, ATTR_FILL); let stroke_row = Item::from_parts(solidified_stroke, stroke_attributes); @@ -1461,7 +1461,7 @@ fn solidify_stroke_core(graphic_list: List) -> List { } } - output.set_attribute(ATTR_EDITOR_MERGED_LAYERS, 0, graphic_list); + output.set_attribute(ATTR_EDITOR_MERGED_LAYERS, 0, Some(graphic_list)); } output @@ -1523,12 +1523,21 @@ fn emit_legacy_lane<'e>( }; let element = output.element(lane).cloned().unwrap_or_default(); - let fill = output.attribute::>(ATTR_FILL, lane).map(|paint| park_paint(arena, paint.clone())).transpose()?; - let stroke = output.attribute::>(ATTR_STROKE, lane).map(|paint| park_paint(arena, paint.clone())).transpose()?; + let fill = output + .attribute::>>(ATTR_FILL, lane) + .and_then(|paint| paint.as_ref()) + .map(|paint| park_paint(arena, paint.clone())) + .transpose()?; + let stroke = output + .attribute::>>(ATTR_STROKE, lane) + .and_then(|paint| paint.as_ref()) + .map(|paint| park_paint(arena, paint.clone())) + .transpose()?; let layer_path: Vec = output.attribute::>(ATTR_EDITOR_LAYER_PATH, lane).cloned().unwrap_or_default(); let layer_path = arena.alloc(layer_path).ok_or_else(exhausted)?.0; let merged_layers = output - .attribute::>(ATTR_EDITOR_MERGED_LAYERS, lane) + .attribute::>>(ATTR_EDITOR_MERGED_LAYERS, lane) + .and_then(|layers| layers.as_ref()) .map(|layers| arena.alloc(layers.clone()).ok_or_else(exhausted).map(|(parked, _)| parked)) .transpose()?; @@ -1813,8 +1822,16 @@ fn flatten_path_core<'e>( bake_paint_transforms(&mut attributes, source_transform); let carrier = List::new_from_item(Item::from_parts(Vector::default(), attributes)); - fill = carrier.attribute::>(ATTR_FILL, 0).map(|paint| park_paint(arena, paint.clone())).transpose()?; - stroke = carrier.attribute::>(ATTR_STROKE, 0).map(|paint| park_paint(arena, paint.clone())).transpose()?; + fill = carrier + .attribute::>>(ATTR_FILL, 0) + .and_then(|paint| paint.as_ref()) + .map(|paint| park_paint(arena, paint.clone())) + .transpose()?; + stroke = carrier + .attribute::>>(ATTR_STROKE, 0) + .and_then(|paint| paint.as_ref()) + .map(|paint| park_paint(arena, paint.clone())) + .transpose()?; // Adopt the last input item's layer so the editor can also bucket clicks under a contributing child layer layer_path = flattened.attribute_cloned_or_default::>(ATTR_EDITOR_LAYER_PATH, primary); @@ -2995,7 +3012,7 @@ fn morph_core(content: List, progression: f64, reverse: bool, distribut 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.insert(ATTR_EDITOR_MERGED_LAYERS, Some(graphic_list_content)); return List::new_from_item(Item::from_parts(endpoint_element.clone(), attributes)); } @@ -3186,13 +3203,13 @@ fn morph_core(content: List, progression: f64, reverse: bool, distribut .with_attribute(ATTR_OPACITY_FILL, lerped_fill) .with_attribute(ATTR_CLIPPING_MASK, lerped_clip) .with_attribute(ATTR_EDITOR_LAYER_PATH, layer_path) - .with_attribute(ATTR_EDITOR_MERGED_LAYERS, graphic_list_content); + .with_attribute(ATTR_EDITOR_MERGED_LAYERS, Some(graphic_list_content)); if let Some(fill) = fill_paint { - item.set_attribute(ATTR_FILL, fill); + item.set_attribute(ATTR_FILL, Some(fill)); } if let Some(stroke) = stroke_paint { - item.set_attribute(ATTR_STROKE, stroke); + item.set_attribute(ATTR_STROKE, Some(stroke)); } List::new_from_item(item) @@ -3885,10 +3902,10 @@ 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_attribute(ATTR_FILL, Some(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_attribute(ATTR_FILL, Some(List::new_from_element(Color::BLUE).into_graphic_list())); let mut content = List::new_from_item(item_a); content.push(item_b);