diff --git a/node-graph/libraries/graphic-types/src/graphic.rs b/node-graph/libraries/graphic-types/src/graphic.rs index 9cca998729..96f8d15c51 100644 --- a/node-graph/libraries/graphic-types/src/graphic.rs +++ b/node-graph/libraries/graphic-types/src/graphic.rs @@ -1,6 +1,8 @@ -use crate::markers::{ATTR_FILL, ATTR_STROKE}; +use crate::markers::{ATTR_FILL, ATTR_STROKE, Fill, Stroke}; +use core_types::attribute::Attribute; use core_types::bounds::{BoundingBox, RenderBoundingBox}; use core_types::graphene_hash::CacheHash; +use core_types::lane::LaneSource; use core_types::list::{AttributeValueDyn, Item, ItemAttributeValues, List}; use core_types::ops::{FromAnchorPosition, ListConvert}; use core_types::render_complexity::RenderComplexity; @@ -200,22 +202,40 @@ pub fn is_paint_present(graphic_list: &List) -> bool { /// 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>> { - paint_at(list, index, attribute) + list.attribute::>>(attribute, index) + .and_then(|optional| optional.as_ref()) .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)) } -/// 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> { - list.attribute::>>(attribute, index).and_then(|optional| optional.as_ref()) -} - /// 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 { - paint_at(list, index, attribute).is_some_and(is_paint_present) + graphic_list_at(list, index, attribute).is_some() +} + +/// Look up the paint graphics stored under the marker `A`, in the canonical `List` form. +pub fn paint_graphics<'a, A, S>(source: &'a S, index: usize) -> Option>> +where + S: LaneSource, + A: Attribute = Option<&'a List>>, +{ + source + .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)) +} + +/// Whether the item carries a non-blank canonical `List` paint under the marker `A`, +/// checked by borrowing without cloning the renderable list. +pub fn has_paint<'a, A, S>(source: &'a S, index: usize) -> bool +where + S: LaneSource, + A: Attribute = Option<&'a List>>, +{ + paint_graphics::(source, index).is_some() } /// Stores a paint attribute in the paint marker's owned form, the only representation paint readers accept. @@ -448,10 +468,10 @@ impl Graphic { let Some(element) = vector.element(index) else { return false }; let opacity: f64 = vector.attribute_cloned_or(ATTR_OPACITY, index, 1.); - 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 = paint_graphics::(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())); + || paint_graphics::(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 }), @@ -463,15 +483,17 @@ impl Graphic { match self { 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<'a, A: Attribute = Option<&'a List>>>(list: &'a List, index: usize) -> bool { + paint_graphics::(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 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 }) } @@ -487,16 +509,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<'a, A: Attribute = Option<&'a List>>>(list: &'a List, index: usize) -> bool { + paint_graphics::(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.); 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 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.), @@ -713,7 +736,8 @@ pub fn run_to_render_list(item: &core_types::r fn push_lane_paint_into_interiors(list: &mut List) { for index in 0..list.len() { for key in [ATTR_FILL, ATTR_STROKE] { - let Some(paint) = paint_at(list, index, key).filter(|paint| is_paint_present(paint)).cloned() else { + let stored = list.attribute::>>(key, index).and_then(|optional| optional.as_ref()); + let Some(paint) = stored.filter(|paint| is_paint_present(paint)).cloned() else { continue; }; let Some(element) = list.element_mut(index) else { continue }; diff --git a/node-graph/nodes/path-bool/src/lib.rs b/node-graph/nodes/path-bool/src/lib.rs index 8cf19473c7..6db3990749 100644 --- a/node-graph/nodes/path-bool/src/lib.rs +++ b/node-graph/nodes/path-bool/src/lib.rs @@ -73,8 +73,8 @@ fn boolean_core<'e>( }; let element = result_vector_list.element(0).cloned().unwrap_or_default(); - let fill = park_paint(graphic_types::graphic::graphic_list_at(&result_vector_list, 0, graphic_types::ATTR_FILL).map(|paint| paint.into_owned()))?; - let stroke = park_paint(graphic_types::graphic::graphic_list_at(&result_vector_list, 0, graphic_types::ATTR_STROKE).map(|paint| paint.into_owned()))?; + let fill = park_paint(graphic_types::graphic::paint_graphics::(&result_vector_list, 0).map(|paint| paint.into_owned()))?; + let stroke = park_paint(graphic_types::graphic::paint_graphics::(&result_vector_list, 0).map(|paint| paint.into_owned()))?; let layer_path: Vec = result_vector_list.attribute::>(ATTR_EDITOR_LAYER_PATH, 0).map(|path| path.clone()).unwrap_or_default(); let layer_path = arena.alloc(layer_path).ok_or_else(exhausted)?.0; // Snapshot the input layers so the renderer can recurse into them for diff --git a/node-graph/nodes/vector/src/vector_nodes.rs b/node-graph/nodes/vector/src/vector_nodes.rs index f9a88dcece..35f04f969f 100644 --- a/node-graph/nodes/vector/src/vector_nodes.rs +++ b/node-graph/nodes/vector/src/vector_nodes.rs @@ -17,7 +17,7 @@ use core_types::transform::Transform; use core_types::uuid::NodeId; use core_types::{ATTR_BLEND_MODE, ATTR_CLIPPING_MASK, ATTR_EDITOR_LAYER_PATH, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_TRANSFORM, CacheHash, Color, Ctx, DeriveCtx, ExtractIndex, InjectIndex}; use glam::{DAffine2, DMat2, DVec2}; -use graphic_types::graphic::{bake_paint_transforms, graphic_list_at, has_paint_at, is_paint_present, set_paint_attribute_at}; +use graphic_types::graphic::{bake_paint_transforms, has_paint, is_paint_present, paint_graphics, set_paint_attribute_at}; use graphic_types::markers::{EditorMergedLayers, Fill, Stroke as StrokeAttr}; use graphic_types::raster_types::{CPU, GPU, Raster}; use graphic_types::{ATTR_EDITOR_MERGED_LAYERS, ATTR_FILL, ATTR_STROKE, Graphic, IntoGraphicList}; @@ -1367,7 +1367,7 @@ fn solidify_stroke_core(graphic_list: List) -> List { 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::(&flattened, index)).collect(); let mut output: List = flattened .into_iter() @@ -3038,13 +3038,13 @@ fn morph_core(content: List, progression: f64, reverse: bool, distribut 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 = paint_graphics::(&content, source_index); + let target = paint_graphics::(&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 = paint_graphics::(&content, source_index); + let target = paint_graphics::(&content, target_index); lerp_graphic(source.as_deref(), target.as_deref(), time) }; @@ -3912,7 +3912,7 @@ mod test { let morphed = super::morph_core(content.into_graphic_list(), 0.5, false, InterpolationDistribution::default(), List::default()); - let fill = graphic_list_at(&morphed, 0, ATTR_FILL).expect("Morph should keep the fill paint at the midpoint"); + let fill = paint_graphics::(&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 {