Store an appearance's paint as a bare Graphic rather than a single-element List<Graphic> (#4435)

Rank the coverage's paint attribute down from List<Graphic> to Graphic
This commit is contained in:
Keavon Chambers
2026-08-14 22:13:00 -07:00
committed by GitHub
parent 69585b25e3
commit ba7cbd83bc
10 changed files with 215 additions and 84 deletions

View File

@@ -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.

View File

@@ -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<Graphic>` type default before the paint connectors ranked down to `Item<Graphic>`.
// 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)

View File

@@ -707,14 +707,14 @@ impl TaggedValue {
}
}
/// The stored form of a paint input's red-slash "no paint" choice: the `List<Graphic>` type default, materializing as an empty paint list.
/// The stored form of a paint input's red-slash "no paint" choice: the `Item<Graphic>` 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<Graphic>` type default created by [`Self::no_paint`] (and by disconnecting a paint wire).
/// Whether this is the `Item<Graphic>` 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))
}
}

View File

@@ -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<Graphic>` paint (implicit default empty, painting nothing), on the
/// Coverage's `Graphic` paint (implicit default `Graphic::None`, painting nothing), on the
/// `List<Coverage>` inside an `Appearance`.
pub const ATTR_PAINT: &str = "paint";
/// Stroke coverage's line thickness (`f64`, implicit default `0.`), on the `Item<Cover>` inside a `Coverage`.

View File

@@ -2,10 +2,13 @@
//! Data uniform across all covers (the paint) rides the outer `List<Coverage>` so columnar presence holds,
//! while cover-specific data rides the inner `Item<Cover>`, 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<Graphic>) -> Item<Coverage> {
/// Builds an appearance row, eliding the paint attribute when it is the default none-paint.
fn cover_row(coverage: Coverage, paint: Graphic) -> Item<Coverage> {
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<Graphic>) -> Item<Coverage> {
impl Appearance {
/// Creates an appearance holding a single coverage with the given paint.
pub fn new_single(coverage: Coverage, paint: List<Graphic>) -> 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<Graphic>> {
self.0.attribute::<List<Graphic>>(ATTR_PAINT, index)
pub fn paint_at(&self, index: usize) -> Option<&Graphic> {
self.0.attribute::<Graphic>(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<Graphic>> {
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<Item = (&Coverage, Option<&List<Graphic>>)> {
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<Item = (&Coverage, Option<&Graphic>)> {
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<Graphic>, 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<Graphic>) -> 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<Stroke>,
pub fill_paint: Option<&'a List<Graphic>>,
pub stroke_paint: Option<&'a List<Graphic>>,
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<T>(item: &mut Item<T>, coverage: Coverage, paint: List<Graphic>, placement: CoverPlacement) {
pub fn stamp_coverage<T>(item: &mut Item<T>, coverage: Coverage, paint: Graphic, placement: CoverPlacement) {
item.attribute_mut_or_insert_default::<Appearance>(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<Graphic>` 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<Graphic> {
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<Vector> {
fn into_paint(self) -> Graphic {
Graphic::VectorList(List::new_from_item(self))
}
}
impl IntoPaint for Item<Raster<CPU>> {
fn into_paint(self) -> Graphic {
Graphic::RasterCPUList(List::new_from_item(self))
}
}
// No Item<Raster<GPU>> impl: GPU rasters have no Default, which the trait bounds require of the element
impl IntoPaint for Item<Color> {
fn into_paint(self) -> Graphic {
Graphic::ColorList(List::new_from_item(self))
}
}
impl IntoPaint for Item<Gradient> {
fn into_paint(self) -> Graphic {
Graphic::GradientList(List::new_from_item(self))
}
}
impl IntoPaint for Item<String> {
fn into_paint(self) -> Graphic {
Graphic::TextList(List::new_from_item(self))
}
}
impl IntoPaint for List<Vector> {
fn into_paint(self) -> Graphic {
Graphic::VectorList(self)
}
}
impl IntoPaint for List<Raster<CPU>> {
fn into_paint(self) -> Graphic {
Graphic::RasterCPUList(self)
}
}
impl IntoPaint for List<Raster<GPU>> {
fn into_paint(self) -> Graphic {
Graphic::RasterGPUList(self)
}
}
impl IntoPaint for List<Color> {
fn into_paint(self) -> Graphic {
Graphic::ColorList(self)
}
}
impl IntoPaint for List<Gradient> {
fn into_paint(self) -> Graphic {
Graphic::GradientList(self)
}
}
impl IntoPaint for List<String> {
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<Graphic> {
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<Color> {
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::<f64>(ATTR_POSITION, 0), Some(&0.25));
}
}

View File

@@ -255,12 +255,10 @@ pub fn bake_paint_transforms(attributes: &mut ItemAttributeValues, transform: DA
}
if let Some(appearance) = attributes.get_mut::<Appearance>(ATTR_APPEARANCE)
&& let Some(paints) = appearance.0.iter_attribute_values_mut::<List<Graphic>>(ATTR_PAINT)
&& let Some(paints) = appearance.0.iter_attribute_values_mut::<Graphic>(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<Vector> = graphics.into_flattened_list();
assert_eq!(flattened.attribute_cloned_or_default::<f64>(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<Vector> = group.into_flattened_list();
assert_eq!(flattened.attribute_cloned_or_default::<f64>(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<Vector> = outer.into_flattened_list();
let color_of = |index: usize| {
let appearance = flattened.attribute::<Appearance>(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()
};

View File

@@ -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};

View File

@@ -1210,10 +1210,12 @@ fn render_vector_item_svg(list: &List<Vector>, 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<List<Graphic>> = fill_paint.map(|paint| List::new_from_element(paint.clone()));
let stroke_graphic_list: Option<List<Graphic>> = 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<Vector>, 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<Vector>, 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<Vector>, index: usize, vector: &Vector, re
// The mask must draw at full alpha so the SVG `<mask>`/`<clipPath>` 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<Vector>, 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<Vector>, 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<Vector> {
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<List<Graphic>> = fill_paint.map(|paint| List::new_from_element(paint.clone()));
let stroke_graphic_list: Option<List<Graphic>> = 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<Vector> {
// 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<Vector> {
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<Vector> {
};
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<Vector> {
// 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);

View File

@@ -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;

View File

@@ -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::<Appearance, _, _>(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<V, F: IntoGraphicList + 'n + Send + 'static>(
async fn fill<V, F: IntoPaint + 'n + Send + 'static>(
_: 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<V, F: IntoGraphicList + 'n + Send + 'static>(
/// The fill to paint the path with.
#[default(Color::BLACK)]
#[implementations(
List<Graphic>, List<Vector>, List<Color>, List<Gradient>, List<Raster<CPU>>, List<Raster<GPU>>,
List<Graphic>, List<Vector>, List<Color>, List<Gradient>, List<Raster<CPU>>, List<Raster<GPU>>,
Item<Graphic>, List<Vector>, List<Color>, List<Gradient>, List<Raster<CPU>>, List<Raster<GPU>>,
Item<Graphic>, List<Vector>, List<Color>, List<Gradient>, List<Raster<CPU>>, List<Raster<GPU>>,
)]
fill: F,
_backup_color: Item<Color>,
@@ -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::<GradientForm>(ATTR_GRADIENT_FORM).is_none() {
for value in gradient.iter_attribute_values_mut_or_default::<GradientForm>(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<V, P: IntoGraphicList + 'n + Send + 'static>(
async fn stroke<V, P: IntoPaint + 'n + Send + 'static>(
_: 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<V, P: IntoGraphicList + 'n + Send + 'static>(
/// The stroke paint.
#[default(Color::BLACK)]
#[implementations(
List<Graphic>, List<Vector>, List<Color>, List<Gradient>, List<Raster<CPU>>, List<Raster<GPU>>,
List<Graphic>, List<Vector>, List<Color>, List<Gradient>, List<Raster<CPU>>, List<Raster<GPU>>,
Item<Graphic>, List<Vector>, List<Color>, List<Gradient>, List<Raster<CPU>>, List<Raster<GPU>>,
Item<Graphic>, List<Vector>, List<Color>, List<Gradient>, List<Raster<CPU>>, List<Raster<GPU>>,
)]
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<I: IntoGraphicList>(
};
// 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
};