diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index 1a63dc4677..0e58cb87a8 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -2760,9 +2760,7 @@ impl DocumentMessageHandler { // A visible stroke needs both renderable geometry (non-zero weight) and paint that draws something let has_stroke = appearance.is_some_and(|appearance| { appearance.first_coverage_of(Cover::Stroke).is_some_and(|coverage| coverage.stroke_params().has_renderable_stroke()) - && appearance - .first_paint_of(Cover::Stroke) - .is_some_and(|paint| paint.element(0).is_some_and(|graphic| !graphic.is_fully_transparent())) + && appearance.first_paint_of(Cover::Stroke).is_some_and(|paint| !paint.is_fully_transparent()) }); // No stroke means there's nothing to solidify. Fill-only layers are already in the desired form, so skip. diff --git a/editor/src/messages/portfolio/document_migration.rs b/editor/src/messages/portfolio/document_migration.rs index 30df3cc785..ac4ada1dc0 100644 --- a/editor/src/messages/portfolio/document_migration.rs +++ b/editor/src/messages/portfolio/document_migration.rs @@ -10,7 +10,7 @@ use glam::{DVec2, IVec2}; use graph_craft::application_io::resource::{DataSource, Resource, ResourceHash, ResourceId}; use graph_craft::document::DocumentNode; use graph_craft::document::{DocumentNodeImplementation, NodeInput, value::TaggedValue}; -use graph_craft::{Type, item}; +use graph_craft::{Type, item, list}; use graphene_std::Color; use graphene_std::ParameterRef; use graphene_std::ProtoNodeIdentifier; @@ -1894,6 +1894,28 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId], .set_input(&InputConnector::node(*node_id, graphene_std::vector::stroke::DashPatternInput), migrated, network_path); } + // The stored no-paint sentinel was the `List` type default before the paint connectors ranked down to `Item`. + // This must run before the stale-List-default cleanup below, which would otherwise adopt the definition's default paint. + { + let legacy_no_paint = TaggedValue::TypeDefault(list!(graphene_std::Graphic)); + let paint_parameters: &[ParameterRef] = &[graphene_std::vector::fill::FillInput.into(), graphene_std::vector::stroke::PaintInput.into()]; + for parameter in paint_parameters { + if reference != DefinitionIdentifier::ProtoNode(parameter.node_identifier.clone()) { + continue; + } + let Some(NodeInput::Value { tagged_value, exposed }) = node.inputs.get(parameter.input_index) else { + continue; + }; + if **tagged_value == legacy_no_paint { + document.network_interface.set_input( + &InputConnector::node_at_index(*node_id, parameter.input_index), + NodeInput::value(TaggedValue::no_paint(), *exposed), + network_path, + ); + } + } + } + // The corner radius became the `BoxCorners` value type; convert any already-shaped rectangle that still stores a legacy corner input if reference == DefinitionIdentifier::ProtoNode(graphene_std::vector::generator_nodes::rectangle::IDENTIFIER) && let Some(corner_input) = node.input(graphene_std::vector::generator_nodes::rectangle::CornerRadiusInput) diff --git a/node-graph/graph-craft/src/document/value.rs b/node-graph/graph-craft/src/document/value.rs index 1ee7f958b7..2d526123ba 100644 --- a/node-graph/graph-craft/src/document/value.rs +++ b/node-graph/graph-craft/src/document/value.rs @@ -707,14 +707,14 @@ impl TaggedValue { } } - /// The stored form of a paint input's red-slash "no paint" choice: the `List` type default, materializing as an empty paint list. + /// The stored form of a paint input's red-slash "no paint" choice: the `Item` type default, materializing as a `Graphic::None` paint. pub fn no_paint() -> Self { - TaggedValue::TypeDefault(list!(Graphic)) + TaggedValue::TypeDefault(item!(Graphic)) } - /// Whether this is the `List` type default created by [`Self::no_paint`] (and by disconnecting a paint wire). + /// Whether this is the `Item` type default created by [`Self::no_paint`] (and by disconnecting a paint wire). pub fn is_no_paint(&self) -> bool { - matches!(self, TaggedValue::TypeDefault(td) if *td == list!(Graphic)) + matches!(self, TaggedValue::TypeDefault(td) if *td == item!(Graphic)) } } diff --git a/node-graph/libraries/core-types/src/list.rs b/node-graph/libraries/core-types/src/list.rs index 7d417fee3c..9bcb006989 100644 --- a/node-graph/libraries/core-types/src/list.rs +++ b/node-graph/libraries/core-types/src/list.rs @@ -80,7 +80,7 @@ pub const ATTR_MIDPOINT: &str = "midpoint"; /// Item's ordered list of paint passes, of type `Appearance`. Earlier coverages paint first, compositing below later ones. pub const ATTR_APPEARANCE: &str = "appearance"; // TODO: Add a "fill_rule" attribute as a sibling of "paint" on the coverage list (uniform across covers) once a FillRule type ships -/// Coverage's `List` paint (implicit default empty, painting nothing), on the +/// Coverage's `Graphic` paint (implicit default `Graphic::None`, painting nothing), on the /// `List` inside an `Appearance`. pub const ATTR_PAINT: &str = "paint"; /// Stroke coverage's line thickness (`f64`, implicit default `0.`), on the `Item` inside a `Coverage`. diff --git a/node-graph/libraries/graphic-types/src/appearance.rs b/node-graph/libraries/graphic-types/src/appearance.rs index 9a6c088323..cb74eebc59 100644 --- a/node-graph/libraries/graphic-types/src/appearance.rs +++ b/node-graph/libraries/graphic-types/src/appearance.rs @@ -2,10 +2,13 @@ //! Data uniform across all covers (the paint) rides the outer `List` so columnar presence holds, //! while cover-specific data rides the inner `Item`, reusing `ATTR_TRANSFORM` for the stroke-authoring space. -use crate::graphic::{Graphic, is_paint_present}; +use crate::graphic::Graphic; +use core_types::Color; use core_types::graphene_hash::CacheHash; use core_types::list::{ATTR_ALIGN, ATTR_APPEARANCE, ATTR_CAP, ATTR_DASH_OFFSET, ATTR_DASH_PATTERN, ATTR_JOIN, ATTR_JOIN_MITER_LIMIT, ATTR_PAINT, ATTR_TRANSFORM, ATTR_WEIGHT, Item, List}; +use raster_types::{CPU, GPU, Raster}; use vector_types::vector::style::{DashPattern, Stroke}; +use vector_types::{Gradient, Vector}; /// The geometry-to-region operator a coverage applies before painting: /// the interior of the geometry (fill) or the region swept along its outline (stroke). @@ -114,10 +117,10 @@ impl Coverage { } } -/// Builds an appearance row, eliding the paint attribute when it draws nothing. -fn cover_row(coverage: Coverage, paint: List) -> Item { +/// Builds an appearance row, eliding the paint attribute when it is the default none-paint. +fn cover_row(coverage: Coverage, paint: Graphic) -> Item { let mut row = Item::new_from_element(coverage); - if is_paint_present(&paint) { + if paint != Graphic::default() { row.set_attribute(ATTR_PAINT, paint); } row @@ -125,7 +128,7 @@ fn cover_row(coverage: Coverage, paint: List) -> Item { impl Appearance { /// Creates an appearance holding a single coverage with the given paint. - pub fn new_single(coverage: Coverage, paint: List) -> Self { + pub fn new_single(coverage: Coverage, paint: Graphic) -> Self { Self(List::new_from_item(cover_row(coverage, paint))) } @@ -161,8 +164,8 @@ impl Appearance { } /// The paint of the coverage at the given index, or `None` if the paint attribute is absent. - pub fn paint_at(&self, index: usize) -> Option<&List> { - self.0.attribute::>(ATTR_PAINT, index) + pub fn paint_at(&self, index: usize) -> Option<&Graphic> { + self.0.attribute::(ATTR_PAINT, index) } /// The index of the first coverage of the given cover in paint order. @@ -176,15 +179,13 @@ impl Appearance { } /// The paint of the first coverage of the given cover, filtered to paint that draws something. - pub fn first_paint_of(&self, cover: Cover) -> Option<&List> { - self.first_index_of(cover).and_then(|index| self.paint_at(index)).filter(|paint| is_paint_present(paint)) + pub fn first_paint_of(&self, cover: Cover) -> Option<&Graphic> { + self.first_index_of(cover).and_then(|index| self.paint_at(index)).filter(|paint| !paint.is_empty()) } /// Iterates the coverages in paint order together with their paint, which is `None` when absent or drawing nothing. - pub fn covers_with_paints(&self) -> impl Iterator>)> { - self.covers() - .enumerate() - .map(|(index, coverage)| (coverage, self.paint_at(index).filter(|paint| is_paint_present(paint)))) + pub fn covers_with_paints(&self) -> impl Iterator)> { + self.covers().enumerate().map(|(index, coverage)| (coverage, self.paint_at(index).filter(|paint| !paint.is_empty()))) } /// Gathers the renderer's per-item reads in one walk of the coverage list. @@ -199,7 +200,7 @@ impl Appearance { } } - let painted = |index| self.paint_at(index).filter(|paint| is_paint_present(paint)); + let painted = |index| self.paint_at(index).filter(|paint| !paint.is_empty()); FillAndStroke { stroke: first_stroke.map(|(_, coverage)| coverage.stroke_params()), fill_paint: first_fill.and_then(painted), @@ -218,12 +219,12 @@ impl Appearance { pub fn has_painted_cover(&self, cover: Cover) -> bool { self.covers() .enumerate() - .any(|(index, coverage)| coverage.cover() == cover && self.paint_at(index).is_some_and(is_paint_present)) + .any(|(index, coverage)| coverage.cover() == cover && self.paint_at(index).is_some_and(|paint| !paint.is_empty())) } /// Replaces the first coverage of the incoming cover in place (keeping its position in the paint order), /// or inserts a new row at the requested end of the paint order if none exists. - pub fn replace_or_insert(&mut self, coverage: Coverage, paint: List, placement: CoverPlacement) { + pub fn replace_or_insert(&mut self, coverage: Coverage, paint: Graphic, placement: CoverPlacement) { if let Some(index) = self.first_index_of(coverage.cover()) { if let Some(element) = self.0.element_mut(index) { *element = coverage; @@ -245,7 +246,7 @@ impl Appearance { /// Sets the paint of the first coverage of the given cover, leaving its other parameters untouched. /// Returns `false` without changing anything if no coverage of that cover exists. - pub fn set_paint_of(&mut self, cover: Cover, paint: List) -> bool { + pub fn set_paint_of(&mut self, cover: Cover, paint: Graphic) -> bool { let Some(index) = self.first_index_of(cover) else { return false }; self.0.set_attribute(ATTR_PAINT, index, paint); true @@ -262,32 +263,117 @@ impl Appearance { #[derive(Debug, Default)] pub struct FillAndStroke<'a> { pub stroke: Option, - pub fill_paint: Option<&'a List>, - pub stroke_paint: Option<&'a List>, + pub fill_paint: Option<&'a Graphic>, + pub stroke_paint: Option<&'a Graphic>, /// Whether the first stroke coverage sits before the first fill in the paint order, painting below it. pub stroke_below: bool, } /// Stamps a coverage into the item's `ATTR_APPEARANCE` cell, creating the attribute if absent. /// The coverage replaces the first same-cover one in place, or lands at the placement end of the paint order. -pub fn stamp_coverage(item: &mut Item, coverage: Coverage, paint: List, placement: CoverPlacement) { +pub fn stamp_coverage(item: &mut Item, coverage: Coverage, paint: Graphic, placement: CoverPlacement) { item.attribute_mut_or_insert_default::(ATTR_APPEARANCE).replace_or_insert(coverage, paint, placement); } +// ================ +// TRAIT: IntoPaint +// ================ + +/// Converts the types accepted by a paint input into the canonical `Graphic` stored in the `ATTR_PAINT` attribute. +/// `List` deliberately has no impl: a multi-element paint is a type error. +pub trait IntoPaint: Clone + Send + Sync + Default + std::fmt::Debug + PartialEq + CacheHash + 'static { + fn into_paint(self) -> Graphic; +} + +impl IntoPaint for Item { + fn into_paint(self) -> Graphic { + // Wrapping to keep the record's attributes would nest the paint as a group, changing how it renders + self.into_element() + } +} + +impl IntoPaint for Item { + fn into_paint(self) -> Graphic { + Graphic::VectorList(List::new_from_item(self)) + } +} + +impl IntoPaint for Item> { + fn into_paint(self) -> Graphic { + Graphic::RasterCPUList(List::new_from_item(self)) + } +} + +// No Item> impl: GPU rasters have no Default, which the trait bounds require of the element + +impl IntoPaint for Item { + fn into_paint(self) -> Graphic { + Graphic::ColorList(List::new_from_item(self)) + } +} + +impl IntoPaint for Item { + fn into_paint(self) -> Graphic { + Graphic::GradientList(List::new_from_item(self)) + } +} + +impl IntoPaint for Item { + fn into_paint(self) -> Graphic { + Graphic::TextList(List::new_from_item(self)) + } +} + +impl IntoPaint for List { + fn into_paint(self) -> Graphic { + Graphic::VectorList(self) + } +} + +impl IntoPaint for List> { + fn into_paint(self) -> Graphic { + Graphic::RasterCPUList(self) + } +} + +impl IntoPaint for List> { + fn into_paint(self) -> Graphic { + Graphic::RasterGPUList(self) + } +} + +impl IntoPaint for List { + fn into_paint(self) -> Graphic { + Graphic::ColorList(self) + } +} + +impl IntoPaint for List { + fn into_paint(self) -> Graphic { + Graphic::GradientList(self) + } +} + +impl IntoPaint for List { + fn into_paint(self) -> Graphic { + Graphic::TextList(self) + } +} + #[cfg(test)] mod tests { use super::*; - use core_types::Color; + use core_types::list::ATTR_POSITION; use glam::{DAffine2, DVec2}; use vector_types::vector::style::{StrokeAlign, StrokeCap, StrokeJoin}; - fn solid_paint(color: Color) -> List { - List::new_from_element(Graphic::ColorList(List::new_from_element(color))) + fn solid_paint(color: Color) -> Graphic { + Graphic::ColorList(List::new_from_element(color)) } fn paint_color(appearance: &Appearance, index: usize) -> Option { let paint = appearance.paint_at(index)?; - let Some(Graphic::ColorList(colors)) = paint.element(0) else { return None }; + let Graphic::ColorList(colors) = paint else { return None }; colors.element(0).copied() } @@ -374,7 +460,7 @@ mod tests { #[test] fn painted_cover_distinguishes_none_paint_from_absence() { let mut appearance = Appearance::default(); - appearance.replace_or_insert(Coverage::new_fill(), List::new_from_element(Graphic::None), CoverPlacement::Above); + appearance.replace_or_insert(Coverage::new_fill(), Graphic::None, CoverPlacement::Above); assert!(appearance.has_cover(Cover::Fill), "a none-painted coverage still exists"); assert!(!appearance.has_painted_cover(Cover::Fill), "a none-painted coverage draws nothing"); @@ -384,4 +470,24 @@ mod tests { appearance.replace_or_insert(Coverage::new_fill(), solid_paint(Color::RED), CoverPlacement::Above); assert!(appearance.has_painted_cover(Cover::Fill)); } + + #[test] + fn list_paint_becomes_one_graphic_holding_every_element() { + let mut colors = List::new_from_element(Color::RED); + colors.push(Item::new_from_element(Color::BLUE)); + + let paint = colors.into_paint(); + let Graphic::ColorList(inner) = &paint else { panic!("expected a color graphic") }; + assert_eq!(inner.len(), 2, "a list paint is one graphic holding all its elements"); + } + + #[test] + fn item_paint_keeps_its_attributes_on_the_inner_row() { + let color = Item::new_from_element(Color::RED).with_attribute(ATTR_POSITION, 0.25_f64); + + let paint = color.into_paint(); + let Graphic::ColorList(inner) = &paint else { panic!("expected a color graphic") }; + assert_eq!(inner.len(), 1); + assert_eq!(inner.attribute::(ATTR_POSITION, 0), Some(&0.25)); + } } diff --git a/node-graph/libraries/graphic-types/src/graphic.rs b/node-graph/libraries/graphic-types/src/graphic.rs index a1130af6c7..77df5fe8d3 100644 --- a/node-graph/libraries/graphic-types/src/graphic.rs +++ b/node-graph/libraries/graphic-types/src/graphic.rs @@ -255,12 +255,10 @@ pub fn bake_paint_transforms(attributes: &mut ItemAttributeValues, transform: DA } if let Some(appearance) = attributes.get_mut::(ATTR_APPEARANCE) - && let Some(paints) = appearance.0.iter_attribute_values_mut::>(ATTR_PAINT) + && let Some(paints) = appearance.0.iter_attribute_values_mut::(ATTR_PAINT) { for paint in paints { - for graphic in paint.iter_element_values_mut() { - bake_graphic_transform(graphic, transform); - } + bake_graphic_transform(paint, transform); } } } @@ -449,14 +447,14 @@ impl Graphic { appearance .covers_with_paints() .filter(|(coverage, _)| coverage.cover() == Cover::Fill) - .all(|(_, paint)| paint.is_none_or(|paint| paint.element(0).is_none_or(Graphic::is_opaque))) + .all(|(_, paint)| paint.is_none_or(Graphic::is_opaque)) }); let strokes_invisible_or_transparent = appearance.is_none_or(|appearance| { appearance .covers_with_paints() .filter(|(coverage, _)| coverage.cover() == Cover::Stroke) - .all(|(coverage, paint)| !coverage.stroke_params().has_renderable_stroke() || paint.is_none_or(|paint| paint.element(0).is_none_or(Graphic::is_fully_transparent))) + .all(|(coverage, paint)| !coverage.stroke_params().has_renderable_stroke() || paint.is_none_or(Graphic::is_fully_transparent)) }); opacity > 1. - f64::EPSILON && fills_opaque_or_absent && strokes_invisible_or_transparent @@ -480,14 +478,14 @@ impl Graphic { && appearance.is_some_and(|appearance| { appearance .covers_with_paints() - .any(|(coverage, paint)| coverage.cover() == Cover::Fill && paint.is_some_and(|paint| paint.element(0).is_some_and(Graphic::is_opaque))) + .any(|(coverage, paint)| coverage.cover() == Cover::Fill && paint.is_some_and(Graphic::is_opaque)) }); let strokes_opaque_or_invisible = appearance.is_none_or(|appearance| { appearance .covers_with_paints() .filter(|(coverage, _)| coverage.cover() == Cover::Stroke) - .all(|(coverage, paint)| !coverage.stroke_params().has_renderable_stroke() || paint.is_some_and(|paint| paint.element(0).is_some_and(Graphic::is_opaque))) + .all(|(coverage, paint)| !coverage.stroke_params().has_renderable_stroke() || paint.is_some_and(Graphic::is_opaque)) }); opacity >= 1. - f64::EPSILON && fill_opaque && strokes_opaque_or_invisible @@ -516,14 +514,14 @@ impl Graphic { appearance .covers_with_paints() .filter(|(coverage, _)| coverage.cover() == Cover::Fill) - .all(|(_, paint)| paint.is_none_or(|paint| paint.element(0).is_none_or(Graphic::is_fully_transparent))) + .all(|(_, paint)| paint.is_none_or(Graphic::is_fully_transparent)) }); let strokes_invisible = appearance.is_none_or(|appearance| { appearance .covers_with_paints() .filter(|(coverage, _)| coverage.cover() == Cover::Stroke) - .all(|(coverage, paint)| !coverage.stroke_params().has_renderable_stroke() || paint.is_none_or(|paint| paint.element(0).is_none_or(Graphic::is_fully_transparent))) + .all(|(coverage, paint)| !coverage.stroke_params().has_renderable_stroke() || paint.is_none_or(Graphic::is_fully_transparent)) }); fills_invisible && strokes_invisible @@ -773,7 +771,7 @@ mod tests { let flattened: List = graphics.into_flattened_list(); assert_eq!(flattened.attribute_cloned_or_default::(ATTR_OPACITY, 0), 0.5); - let mut group = List::new_from_element(Graphic::GraphicList(GraphicList(List::new_from_element(vector_graphic())))); + let mut group = List::new_from_element(Graphic::GraphicList(List::new_from_element(vector_graphic()))); group.set_attribute(ATTR_OPACITY, 0, 0.5_f64); let flattened: List = group.into_flattened_list(); assert_eq!(flattened.attribute_cloned_or_default::(ATTR_OPACITY, 0), 0.5); @@ -784,7 +782,7 @@ mod tests { fn flatten_cascades_into_padded_empty_appearance_rows() { use core_types::Color; - let solid = |color: Color| List::new_from_element(Graphic::ColorList(List::new_from_element(color))); + let solid = |color: Color| Graphic::ColorList(List::new_from_element(color)); // Declaring an appearance on row 0 forces the column, padding row 1 with the empty appearance let mut inner = List::new(); @@ -798,7 +796,7 @@ mod tests { let flattened: List = outer.into_flattened_list(); let color_of = |index: usize| { let appearance = flattened.attribute::(ATTR_APPEARANCE, index)?; - let Some(Graphic::ColorList(colors)) = appearance.paint_at(0)?.element(0) else { return None }; + let Graphic::ColorList(colors) = appearance.paint_at(0)? else { return None }; colors.element(0).copied() }; diff --git a/node-graph/libraries/graphic-types/src/lib.rs b/node-graph/libraries/graphic-types/src/lib.rs index 27f338482a..c04450efa8 100644 --- a/node-graph/libraries/graphic-types/src/lib.rs +++ b/node-graph/libraries/graphic-types/src/lib.rs @@ -8,7 +8,7 @@ pub use raster_types; pub use vector_types; // Re-export commonly used types at the crate root -pub use appearance::{Appearance, Cover, CoverPlacement, Coverage, FillAndStroke, stamp_coverage}; +pub use appearance::{Appearance, Cover, CoverPlacement, Coverage, FillAndStroke, IntoPaint, stamp_coverage}; pub use artboard::Artboard; pub use graphic::{Graphic, IntoGraphicList, TryFromGraphic, Vector}; diff --git a/node-graph/libraries/rendering/src/renderer.rs b/node-graph/libraries/rendering/src/renderer.rs index c0130b56ac..cecc2dfec9 100644 --- a/node-graph/libraries/rendering/src/renderer.rs +++ b/node-graph/libraries/rendering/src/renderer.rs @@ -1210,10 +1210,12 @@ fn render_vector_item_svg(list: &List, index: usize, vector: &Vector, re let appearance = own_appearance.or(render_params.inherited_appearance.as_ref()); let FillAndStroke { stroke: stroke_params, - fill_paint: fill_graphic_list, - stroke_paint: stroke_graphic_list, + fill_paint, + stroke_paint, stroke_below: wants_stroke_below, } = appearance.map(Appearance::fill_and_stroke).unwrap_or_default(); + let fill_graphic_list: Option> = fill_paint.map(|paint| List::new_from_element(paint.clone())); + let stroke_graphic_list: Option> = stroke_paint.map(|paint| List::new_from_element(paint.clone())); // Only consider strokes with non-zero weight, since default strokes with zero weight would prevent assigning the correct stroke transform let has_real_stroke = stroke_params.as_ref().filter(|stroke| stroke.weight() > 0.); @@ -1245,8 +1247,8 @@ fn render_vector_item_svg(list: &List, index: usize, vector: &Vector, re MaskType::Mask }; - let fill_graphic = fill_graphic_list.and_then(|l| l.element(0)); - let stroke_graphic = stroke_graphic_list.and_then(|l| l.element(0)); + let fill_graphic = fill_graphic_list.as_ref().and_then(|l| l.element(0)); + 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()); let can_draw_aligned_stroke = path_is_closed @@ -1262,7 +1264,7 @@ fn render_vector_item_svg(list: &List, index: usize, vector: &Vector, re emit_svg_fill_path( render, path.clone(), - fill_graphic_list, + fill_graphic_list.as_ref(), item_transform, element_transform, applied_stroke_transform, @@ -1279,7 +1281,7 @@ fn render_vector_item_svg(list: &List, index: usize, vector: &Vector, re // 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); - let black_fill = List::new_from_element(Graphic::ColorList(List::new_from_element(Color::BLACK))); + let black_fill = Graphic::ColorList(List::new_from_element(Color::BLACK)); mask_item.set_attribute(ATTR_APPEARANCE, Appearance::new_single(Coverage::new_fill(), black_fill)); let vector_item = List::new_from_item(mask_item); @@ -1294,7 +1296,7 @@ fn render_vector_item_svg(list: &List, index: usize, vector: &Vector, re emit_svg_fill_path( render, face_d, - fill_graphic_list, + fill_graphic_list.as_ref(), item_transform, element_transform, applied_stroke_transform, @@ -1407,7 +1409,7 @@ fn render_vector_item_svg(list: &List, index: usize, vector: &Vector, re emit_svg_fill_path( render, path.clone(), - fill_graphic_list, + fill_graphic_list.as_ref(), item_transform, element_transform, applied_stroke_transform, @@ -1481,10 +1483,12 @@ impl Render for List { let appearance = own_appearance.or(render_params.inherited_appearance.as_ref()); let FillAndStroke { stroke: stroke_params, - fill_paint: fill_graphic_list, - stroke_paint: stroke_graphic_list, + fill_paint, + stroke_paint, stroke_below: wants_stroke_below, } = appearance.map(Appearance::fill_and_stroke).unwrap_or_default(); + let fill_graphic_list: Option> = fill_paint.map(|paint| List::new_from_element(paint.clone())); + let stroke_graphic_list: Option> = stroke_paint.map(|paint| List::new_from_element(paint.clone())); let has_real_stroke = stroke_params.as_ref().filter(|stroke| stroke.weight() > 0.); // A cascaded coverage records its stroke space in the ancestor's coordinates, so this item authors its own @@ -1524,7 +1528,7 @@ impl Render for List { // Used by both the blend-layer clip rect inflation below (as `max_aabb_inflation`'s `path_is_closed` arg, equivalent here since // the function ignores the arg for Center align) and the `SrcIn`/`SrcOut` aligned-stroke branch further down. let stroke = stroke_params.as_ref(); - let stroke_fully_transparent = stroke_graphic_list.is_none_or(|l| l.element(0).is_none_or(|g| g.is_fully_transparent())); + let stroke_fully_transparent = stroke_graphic_list.as_ref().is_none_or(|l| l.element(0).is_none_or(|g| g.is_fully_transparent())); let can_draw_aligned_stroke = !stroke_fully_transparent && stroke.is_some_and(|s| s.has_renderable_stroke() && s.align.is_not_centered()) && element.stroke_bezier_paths().all(|p| p.closed()); @@ -1580,7 +1584,7 @@ impl Render for List { let use_layer = can_draw_aligned_stroke; let do_fill_path = |scene: &mut Scene, context: &mut RenderContext, path: &kurbo::BezPath, fill_rule: peniko::Fill| { - let Some(fill_graphic) = fill_graphic_list else { return }; + let Some(fill_graphic) = fill_graphic_list.as_ref() else { return }; for paint_index in 0..fill_graphic.len() { let Some(paint) = fill_graphic.element(paint_index) else { continue }; @@ -1634,7 +1638,7 @@ impl Render for List { }; let do_stroke = |scene: &mut Scene, width_scale: f64, context: &mut RenderContext| { - let Some(stroke_graphic_list) = stroke_graphic_list else { return }; + let Some(stroke_graphic_list) = stroke_graphic_list.as_ref() else { return }; let Some(stroke) = stroke else { return }; for paint_index in 0..stroke_graphic_list.len() { @@ -1713,7 +1717,7 @@ 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); - let black_fill = List::new_from_element(Graphic::ColorList(List::new_from_element(Color::BLACK))); + let black_fill = Graphic::ColorList(List::new_from_element(Color::BLACK)); mask_item.set_attribute(ATTR_APPEARANCE, Appearance::new_single(Coverage::new_fill(), black_fill)); let vector_list = List::new_from_item(mask_item); diff --git a/node-graph/nodes/gstd/src/lib.rs b/node-graph/nodes/gstd/src/lib.rs index 3761fef610..0beebffa32 100644 --- a/node-graph/nodes/gstd/src/lib.rs +++ b/node-graph/nodes/gstd/src/lib.rs @@ -12,7 +12,7 @@ pub use graphene_application_io as application_io; pub use graphene_core; pub use graphene_core::debug; pub use graphic_nodes; -pub use graphic_types::{Appearance, Artboard, Cover, CoverPlacement, Coverage, Graphic, Vector, stamp_coverage}; +pub use graphic_types::{Appearance, Artboard, Cover, CoverPlacement, Coverage, Graphic, IntoPaint, Vector, stamp_coverage}; pub use math_nodes; pub use path_bool_nodes; pub use raster_nodes; diff --git a/node-graph/nodes/vector/src/vector_nodes.rs b/node-graph/nodes/vector/src/vector_nodes.rs index 343f2ac3e8..9bfb447d07 100644 --- a/node-graph/nodes/vector/src/vector_nodes.rs +++ b/node-graph/nodes/vector/src/vector_nodes.rs @@ -15,7 +15,7 @@ use glam::{DAffine2, DMat2, DVec2}; use graphic_types::Vector; use graphic_types::graphic::{bake_paint_transforms, is_paint_present}; use graphic_types::raster_types::{CPU, GPU, Raster}; -use graphic_types::{Appearance, Cover, CoverPlacement, Coverage, Graphic, IntoGraphicList, stamp_coverage}; +use graphic_types::{Appearance, Cover, CoverPlacement, Coverage, Graphic, IntoGraphicList, IntoPaint, stamp_coverage}; use kurbo::simplify::{SimplifyOptions, simplify_bezpath}; use kurbo::{Affine, BezPath, DEFAULT_ACCURACY, Line, ParamCurve, ParamCurveArclen, PathEl, PathSeg, Shape}; use rand::{Rng, SeedableRng}; @@ -239,7 +239,7 @@ where }; let color = evaluator.evaluate(factor); - let color_paint = List::new_from_element(color).into_graphic_list(); + let color_paint = Graphic::ColorList(List::new_from_element(color)); if fill { vector_list.with_attribute_mut_or_default::(ATTR_APPEARANCE, index, |appearance| { @@ -268,7 +268,7 @@ where /// Applies a fill style to the vector content, giving an appearance to the area within the interior of the geometry. #[node_macro::node(category("Vector: Style"), path(graphene_core::vector), properties("fill_properties"))] -async fn fill( +async fn fill( _: impl Ctx, /// The content with vector paths to apply the fill style to. #[implementations(Vector, Vector, Vector, Vector, Vector, Vector, Graphic, Graphic, Graphic, Graphic, Graphic, Graphic)] @@ -276,8 +276,8 @@ async fn fill( /// The fill to paint the path with. #[default(Color::BLACK)] #[implementations( - List, List, List, List, List>, List>, - List, List, List, List, List>, List>, + Item, List, List, List, List>, List>, + Item, List, List, List, List>, List>, )] fill: F, _backup_color: Item, @@ -293,12 +293,10 @@ where let (_has_transform, _transform) = (_has_transform.into_element(), *_transform.element()); let mut content = content; - let mut fill = fill.into_graphic_list(); + let mut fill = fill.into_paint(); // Stamp the gradient styling inputs onto any gradient paint missing them, whether the paint arrived as a picker value or a wire - for graphic in fill.iter_element_values_mut() { - let Graphic::GradientList(gradient) = graphic else { continue }; - + if let Graphic::GradientList(gradient) = &mut fill { if gradient.iter_attribute_values::(ATTR_GRADIENT_FORM).is_none() { for value in gradient.iter_attribute_values_mut_or_default::(ATTR_GRADIENT_FORM) { *value = _gradient_form; @@ -344,7 +342,7 @@ where /// Applies a stroke style to the vector content, giving an appearance to the area within the outline of the geometry. #[node_macro::node(category("Vector: Style"), path(graphene_core::vector), properties("stroke_properties"))] -async fn stroke( +async fn stroke( _: impl Ctx, /// The content with vector paths to apply the stroke style to. #[implementations(Vector, Vector, Vector, Vector, Vector, Vector, Graphic, Graphic, Graphic, Graphic, Graphic, Graphic)] @@ -352,8 +350,8 @@ async fn stroke( /// The stroke paint. #[default(Color::BLACK)] #[implementations( - List, List, List, List, List>, List>, - List, List, List, List, List>, List>, + Item, List, List, List, List>, List>, + Item, List, List, List, List>, List>, )] paint: P, /// The stroke thickness. @@ -400,7 +398,7 @@ where transform: DAffine2::IDENTITY, }; - let paint = paint.into_graphic_list(); + let paint = paint.into_paint(); // The coverage records the stroke's authoring space, so the item transform is composed in. Its translation // cancels out in every consumer, so it is cleared to let an otherwise-identity capture elide. @@ -2662,7 +2660,12 @@ async fn morph( }; // An unmatched side falls to `None` here, which `lerp_graphic` fades against transparent - let paint = lerp_graphic(source_index.and_then(|index| a.paint_at(index)), target_index.and_then(|index| b.paint_at(index)), time).unwrap_or_default(); + let source_paint = source_index.and_then(|index| a.paint_at(index)).map(|paint| List::new_from_element(paint.clone())); + let target_paint = target_index.and_then(|index| b.paint_at(index)).map(|paint| List::new_from_element(paint.clone())); + let paint = lerp_graphic(source_paint.as_ref(), target_paint.as_ref(), time) + .and_then(|list| list.into_iter().next()) + .map(Item::into_element) + .unwrap_or_default(); result.replace_or_insert(coverage, paint, CoverPlacement::Above); } @@ -3886,7 +3889,7 @@ mod test { v }; - let solid_fill = |color: Color| Appearance::new_single(Coverage::new_fill(), List::new_from_element(color).into_graphic_list()); + let solid_fill = |color: Color| Appearance::new_single(Coverage::new_fill(), List::new_from_element(color).into_paint()); let item_a = Item::new_from_element(rect()) .with_attribute(ATTR_TRANSFORM, DAffine2::IDENTITY) .with_attribute(ATTR_APPEARANCE, solid_fill(Color::RED)); @@ -3912,8 +3915,8 @@ mod test { let fill = appearance.first_paint_of(Cover::Fill).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::ColorList(colors)) = fill.element(0) else { - panic!("Expected a solid color fill, got {:?}", fill.element(0)); + let Graphic::ColorList(colors) = fill else { + panic!("Expected a solid color fill, got {fill:?}"); }; let color = *colors.element(0).expect("Color present"); assert!(color.r() > 0. && color.b() > 0., "Fill should be a red-to-blue blend, got {color:?}"); @@ -3929,8 +3932,8 @@ mod test { let paint_color = |appearance: &Appearance, cover| { let paint = appearance.first_paint_of(cover).expect("Morph should keep both paints at the midpoint"); - let Some(Graphic::ColorList(colors)) = paint.element(0) else { - panic!("Expected a solid color paint, got {:?}", paint.element(0)); + let Graphic::ColorList(colors) = paint else { + panic!("Expected a solid color paint, got {paint:?}"); }; *colors.element(0).expect("Color present") }; @@ -3938,8 +3941,8 @@ mod test { // The two endpoints list their covers in opposite paint orders, which pairing by position would cross let appearance = |fill: Color, stroke: Color, stroke_placement| { let mut appearance = Appearance::default(); - appearance.replace_or_insert(Coverage::new_fill(), List::new_from_element(fill).into_graphic_list(), CoverPlacement::Above); - appearance.replace_or_insert(Coverage::new_stroke(&Stroke::new(4.)), List::new_from_element(stroke).into_graphic_list(), stroke_placement); + appearance.replace_or_insert(Coverage::new_fill(), List::new_from_element(fill).into_paint(), CoverPlacement::Above); + appearance.replace_or_insert(Coverage::new_stroke(&Stroke::new(4.)), List::new_from_element(stroke).into_paint(), stroke_placement); appearance };