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 // A visible stroke needs both renderable geometry (non-zero weight) and paint that draws something
let has_stroke = appearance.is_some_and(|appearance| { 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_coverage_of(Cover::Stroke).is_some_and(|coverage| coverage.stroke_params().has_renderable_stroke())
&& appearance && appearance.first_paint_of(Cover::Stroke).is_some_and(|paint| !paint.is_fully_transparent())
.first_paint_of(Cover::Stroke)
.is_some_and(|paint| paint.element(0).is_some_and(|graphic| !graphic.is_fully_transparent()))
}); });
// No stroke means there's nothing to solidify. Fill-only layers are already in the desired form, so skip. // 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::application_io::resource::{DataSource, Resource, ResourceHash, ResourceId};
use graph_craft::document::DocumentNode; use graph_craft::document::DocumentNode;
use graph_craft::document::{DocumentNodeImplementation, NodeInput, value::TaggedValue}; 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::Color;
use graphene_std::ParameterRef; use graphene_std::ParameterRef;
use graphene_std::ProtoNodeIdentifier; 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); .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 // 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) if reference == DefinitionIdentifier::ProtoNode(graphene_std::vector::generator_nodes::rectangle::IDENTIFIER)
&& let Some(corner_input) = node.input(graphene_std::vector::generator_nodes::rectangle::CornerRadiusInput) && 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 { 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 { 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. /// Item's ordered list of paint passes, of type `Appearance`. Earlier coverages paint first, compositing below later ones.
pub const ATTR_APPEARANCE: &str = "appearance"; 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 // 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`. /// `List<Coverage>` inside an `Appearance`.
pub const ATTR_PAINT: &str = "paint"; pub const ATTR_PAINT: &str = "paint";
/// Stroke coverage's line thickness (`f64`, implicit default `0.`), on the `Item<Cover>` inside a `Coverage`. /// 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, //! 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. //! 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::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 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::vector::style::{DashPattern, Stroke};
use vector_types::{Gradient, Vector};
/// The geometry-to-region operator a coverage applies before painting: /// The geometry-to-region operator a coverage applies before painting:
/// the interior of the geometry (fill) or the region swept along its outline (stroke). /// 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. /// Builds an appearance row, eliding the paint attribute when it is the default none-paint.
fn cover_row(coverage: Coverage, paint: List<Graphic>) -> Item<Coverage> { fn cover_row(coverage: Coverage, paint: Graphic) -> Item<Coverage> {
let mut row = Item::new_from_element(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.set_attribute(ATTR_PAINT, paint);
} }
row row
@@ -125,7 +128,7 @@ fn cover_row(coverage: Coverage, paint: List<Graphic>) -> Item<Coverage> {
impl Appearance { impl Appearance {
/// Creates an appearance holding a single coverage with the given paint. /// 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))) 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. /// 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>> { pub fn paint_at(&self, index: usize) -> Option<&Graphic> {
self.0.attribute::<List<Graphic>>(ATTR_PAINT, index) self.0.attribute::<Graphic>(ATTR_PAINT, index)
} }
/// The index of the first coverage of the given cover in paint order. /// 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. /// 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>> { pub fn first_paint_of(&self, cover: Cover) -> Option<&Graphic> {
self.first_index_of(cover).and_then(|index| self.paint_at(index)).filter(|paint| is_paint_present(paint)) 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. /// 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>>)> { pub fn covers_with_paints(&self) -> impl Iterator<Item = (&Coverage, Option<&Graphic>)> {
self.covers() self.covers().enumerate().map(|(index, coverage)| (coverage, self.paint_at(index).filter(|paint| !paint.is_empty())))
.enumerate()
.map(|(index, coverage)| (coverage, self.paint_at(index).filter(|paint| is_paint_present(paint))))
} }
/// Gathers the renderer's per-item reads in one walk of the coverage list. /// 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 { FillAndStroke {
stroke: first_stroke.map(|(_, coverage)| coverage.stroke_params()), stroke: first_stroke.map(|(_, coverage)| coverage.stroke_params()),
fill_paint: first_fill.and_then(painted), fill_paint: first_fill.and_then(painted),
@@ -218,12 +219,12 @@ impl Appearance {
pub fn has_painted_cover(&self, cover: Cover) -> bool { pub fn has_painted_cover(&self, cover: Cover) -> bool {
self.covers() self.covers()
.enumerate() .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), /// 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. /// 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(index) = self.first_index_of(coverage.cover()) {
if let Some(element) = self.0.element_mut(index) { if let Some(element) = self.0.element_mut(index) {
*element = coverage; *element = coverage;
@@ -245,7 +246,7 @@ impl Appearance {
/// Sets the paint of the first coverage of the given cover, leaving its other parameters untouched. /// 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. /// 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 }; let Some(index) = self.first_index_of(cover) else { return false };
self.0.set_attribute(ATTR_PAINT, index, paint); self.0.set_attribute(ATTR_PAINT, index, paint);
true true
@@ -262,32 +263,117 @@ impl Appearance {
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub struct FillAndStroke<'a> { pub struct FillAndStroke<'a> {
pub stroke: Option<Stroke>, pub stroke: Option<Stroke>,
pub fill_paint: Option<&'a List<Graphic>>, pub fill_paint: Option<&'a Graphic>,
pub stroke_paint: Option<&'a List<Graphic>>, pub stroke_paint: Option<&'a Graphic>,
/// Whether the first stroke coverage sits before the first fill in the paint order, painting below it. /// Whether the first stroke coverage sits before the first fill in the paint order, painting below it.
pub stroke_below: bool, pub stroke_below: bool,
} }
/// Stamps a coverage into the item's `ATTR_APPEARANCE` cell, creating the attribute if absent. /// 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. /// 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); 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)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use core_types::Color; use core_types::list::ATTR_POSITION;
use glam::{DAffine2, DVec2}; use glam::{DAffine2, DVec2};
use vector_types::vector::style::{StrokeAlign, StrokeCap, StrokeJoin}; use vector_types::vector::style::{StrokeAlign, StrokeCap, StrokeJoin};
fn solid_paint(color: Color) -> List<Graphic> { fn solid_paint(color: Color) -> Graphic {
List::new_from_element(Graphic::ColorList(List::new_from_element(color))) Graphic::ColorList(List::new_from_element(color))
} }
fn paint_color(appearance: &Appearance, index: usize) -> Option<Color> { fn paint_color(appearance: &Appearance, index: usize) -> Option<Color> {
let paint = appearance.paint_at(index)?; 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() colors.element(0).copied()
} }
@@ -374,7 +460,7 @@ mod tests {
#[test] #[test]
fn painted_cover_distinguishes_none_paint_from_absence() { fn painted_cover_distinguishes_none_paint_from_absence() {
let mut appearance = Appearance::default(); 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_cover(Cover::Fill), "a none-painted coverage still exists");
assert!(!appearance.has_painted_cover(Cover::Fill), "a none-painted coverage draws nothing"); 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); appearance.replace_or_insert(Coverage::new_fill(), solid_paint(Color::RED), CoverPlacement::Above);
assert!(appearance.has_painted_cover(Cover::Fill)); 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) 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 paint in paints {
for graphic in paint.iter_element_values_mut() { bake_graphic_transform(paint, transform);
bake_graphic_transform(graphic, transform);
}
} }
} }
} }
@@ -449,14 +447,14 @@ impl Graphic {
appearance appearance
.covers_with_paints() .covers_with_paints()
.filter(|(coverage, _)| coverage.cover() == Cover::Fill) .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| { let strokes_invisible_or_transparent = appearance.is_none_or(|appearance| {
appearance appearance
.covers_with_paints() .covers_with_paints()
.filter(|(coverage, _)| coverage.cover() == Cover::Stroke) .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 opacity > 1. - f64::EPSILON && fills_opaque_or_absent && strokes_invisible_or_transparent
@@ -480,14 +478,14 @@ impl Graphic {
&& appearance.is_some_and(|appearance| { && appearance.is_some_and(|appearance| {
appearance appearance
.covers_with_paints() .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| { let strokes_opaque_or_invisible = appearance.is_none_or(|appearance| {
appearance appearance
.covers_with_paints() .covers_with_paints()
.filter(|(coverage, _)| coverage.cover() == Cover::Stroke) .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 opacity >= 1. - f64::EPSILON && fill_opaque && strokes_opaque_or_invisible
@@ -516,14 +514,14 @@ impl Graphic {
appearance appearance
.covers_with_paints() .covers_with_paints()
.filter(|(coverage, _)| coverage.cover() == Cover::Fill) .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| { let strokes_invisible = appearance.is_none_or(|appearance| {
appearance appearance
.covers_with_paints() .covers_with_paints()
.filter(|(coverage, _)| coverage.cover() == Cover::Stroke) .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 fills_invisible && strokes_invisible
@@ -773,7 +771,7 @@ mod tests {
let flattened: List<Vector> = graphics.into_flattened_list(); let flattened: List<Vector> = graphics.into_flattened_list();
assert_eq!(flattened.attribute_cloned_or_default::<f64>(ATTR_OPACITY, 0), 0.5); 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); group.set_attribute(ATTR_OPACITY, 0, 0.5_f64);
let flattened: List<Vector> = group.into_flattened_list(); let flattened: List<Vector> = group.into_flattened_list();
assert_eq!(flattened.attribute_cloned_or_default::<f64>(ATTR_OPACITY, 0), 0.5); 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() { fn flatten_cascades_into_padded_empty_appearance_rows() {
use core_types::Color; 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 // Declaring an appearance on row 0 forces the column, padding row 1 with the empty appearance
let mut inner = List::new(); let mut inner = List::new();
@@ -798,7 +796,7 @@ mod tests {
let flattened: List<Vector> = outer.into_flattened_list(); let flattened: List<Vector> = outer.into_flattened_list();
let color_of = |index: usize| { let color_of = |index: usize| {
let appearance = flattened.attribute::<Appearance>(ATTR_APPEARANCE, index)?; 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() colors.element(0).copied()
}; };

View File

@@ -8,7 +8,7 @@ pub use raster_types;
pub use vector_types; pub use vector_types;
// Re-export commonly used types at the crate root // 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 artboard::Artboard;
pub use graphic::{Graphic, IntoGraphicList, TryFromGraphic, Vector}; 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 appearance = own_appearance.or(render_params.inherited_appearance.as_ref());
let FillAndStroke { let FillAndStroke {
stroke: stroke_params, stroke: stroke_params,
fill_paint: fill_graphic_list, fill_paint,
stroke_paint: stroke_graphic_list, stroke_paint,
stroke_below: wants_stroke_below, stroke_below: wants_stroke_below,
} = appearance.map(Appearance::fill_and_stroke).unwrap_or_default(); } = 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 // 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.); 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 MaskType::Mask
}; };
let fill_graphic = fill_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.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 path_is_closed = vector.stroke_bezier_paths().all(|path| path.closed());
let can_draw_aligned_stroke = path_is_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( emit_svg_fill_path(
render, render,
path.clone(), path.clone(),
fill_graphic_list, fill_graphic_list.as_ref(),
item_transform, item_transform,
element_transform, element_transform,
applied_stroke_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 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. // 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 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)); mask_item.set_attribute(ATTR_APPEARANCE, Appearance::new_single(Coverage::new_fill(), black_fill));
let vector_item = List::new_from_item(mask_item); 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( emit_svg_fill_path(
render, render,
face_d, face_d,
fill_graphic_list, fill_graphic_list.as_ref(),
item_transform, item_transform,
element_transform, element_transform,
applied_stroke_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( emit_svg_fill_path(
render, render,
path.clone(), path.clone(),
fill_graphic_list, fill_graphic_list.as_ref(),
item_transform, item_transform,
element_transform, element_transform,
applied_stroke_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 appearance = own_appearance.or(render_params.inherited_appearance.as_ref());
let FillAndStroke { let FillAndStroke {
stroke: stroke_params, stroke: stroke_params,
fill_paint: fill_graphic_list, fill_paint,
stroke_paint: stroke_graphic_list, stroke_paint,
stroke_below: wants_stroke_below, stroke_below: wants_stroke_below,
} = appearance.map(Appearance::fill_and_stroke).unwrap_or_default(); } = 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.); 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 // 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 // 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. // 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 = 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 = 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()); !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 use_layer = can_draw_aligned_stroke;
let do_fill_path = |scene: &mut Scene, context: &mut RenderContext, path: &kurbo::BezPath, fill_rule: peniko::Fill| { 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() { for paint_index in 0..fill_graphic.len() {
let Some(paint) = fill_graphic.element(paint_index) else { continue }; 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 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 }; let Some(stroke) = stroke else { return };
for paint_index in 0..stroke_graphic_list.len() { 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 mask must draw at full alpha so `SrcOut` fully zeroes the path interior.
// The outer opacity/blend layer (above) handles the user-set opacity. // 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 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)); mask_item.set_attribute(ATTR_APPEARANCE, Appearance::new_single(Coverage::new_fill(), black_fill));
let vector_list = List::new_from_item(mask_item); 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;
pub use graphene_core::debug; pub use graphene_core::debug;
pub use graphic_nodes; 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 math_nodes;
pub use path_bool_nodes; pub use path_bool_nodes;
pub use raster_nodes; pub use raster_nodes;

View File

@@ -15,7 +15,7 @@ use glam::{DAffine2, DMat2, DVec2};
use graphic_types::Vector; use graphic_types::Vector;
use graphic_types::graphic::{bake_paint_transforms, is_paint_present}; use graphic_types::graphic::{bake_paint_transforms, is_paint_present};
use graphic_types::raster_types::{CPU, GPU, Raster}; 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::simplify::{SimplifyOptions, simplify_bezpath};
use kurbo::{Affine, BezPath, DEFAULT_ACCURACY, Line, ParamCurve, ParamCurveArclen, PathEl, PathSeg, Shape}; use kurbo::{Affine, BezPath, DEFAULT_ACCURACY, Line, ParamCurve, ParamCurveArclen, PathEl, PathSeg, Shape};
use rand::{Rng, SeedableRng}; use rand::{Rng, SeedableRng};
@@ -239,7 +239,7 @@ where
}; };
let color = evaluator.evaluate(factor); 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 { if fill {
vector_list.with_attribute_mut_or_default::<Appearance, _, _>(ATTR_APPEARANCE, index, |appearance| { 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. /// 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"))] #[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, _: impl Ctx,
/// The content with vector paths to apply the fill style to. /// The content with vector paths to apply the fill style to.
#[implementations(Vector, Vector, Vector, Vector, Vector, Vector, Graphic, Graphic, Graphic, Graphic, Graphic, Graphic)] #[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. /// The fill to paint the path with.
#[default(Color::BLACK)] #[default(Color::BLACK)]
#[implementations( #[implementations(
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>>,
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>>,
)] )]
fill: F, fill: F,
_backup_color: Item<Color>, _backup_color: Item<Color>,
@@ -293,12 +293,10 @@ where
let (_has_transform, _transform) = (_has_transform.into_element(), *_transform.element()); let (_has_transform, _transform) = (_has_transform.into_element(), *_transform.element());
let mut content = content; 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 // 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() { if let Graphic::GradientList(gradient) = &mut fill {
let Graphic::GradientList(gradient) = graphic else { continue };
if gradient.iter_attribute_values::<GradientForm>(ATTR_GRADIENT_FORM).is_none() { 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) { for value in gradient.iter_attribute_values_mut_or_default::<GradientForm>(ATTR_GRADIENT_FORM) {
*value = _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. /// 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"))] #[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, _: impl Ctx,
/// The content with vector paths to apply the stroke style to. /// The content with vector paths to apply the stroke style to.
#[implementations(Vector, Vector, Vector, Vector, Vector, Vector, Graphic, Graphic, Graphic, Graphic, Graphic, Graphic)] #[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. /// The stroke paint.
#[default(Color::BLACK)] #[default(Color::BLACK)]
#[implementations( #[implementations(
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>>,
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>>,
)] )]
paint: P, paint: P,
/// The stroke thickness. /// The stroke thickness.
@@ -400,7 +398,7 @@ where
transform: DAffine2::IDENTITY, 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 // 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. // 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 // 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); result.replace_or_insert(coverage, paint, CoverPlacement::Above);
} }
@@ -3886,7 +3889,7 @@ mod test {
v 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()) let item_a = Item::new_from_element(rect())
.with_attribute(ATTR_TRANSFORM, DAffine2::IDENTITY) .with_attribute(ATTR_TRANSFORM, DAffine2::IDENTITY)
.with_attribute(ATTR_APPEARANCE, solid_fill(Color::RED)); .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"); 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 // Interpolated color between red and blue should have >0 value on both R and B
let Some(Graphic::ColorList(colors)) = fill.element(0) else { let Graphic::ColorList(colors) = fill else {
panic!("Expected a solid color fill, got {:?}", fill.element(0)); panic!("Expected a solid color fill, got {fill:?}");
}; };
let color = *colors.element(0).expect("Color present"); 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:?}"); 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_color = |appearance: &Appearance, cover| {
let paint = appearance.first_paint_of(cover).expect("Morph should keep both paints at the midpoint"); 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 { let Graphic::ColorList(colors) = paint else {
panic!("Expected a solid color paint, got {:?}", paint.element(0)); panic!("Expected a solid color paint, got {paint:?}");
}; };
*colors.element(0).expect("Color present") *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 // 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 appearance = |fill: Color, stroke: Color, stroke_placement| {
let mut appearance = Appearance::default(); 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_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_graphic_list(), stroke_placement); appearance.replace_or_insert(Coverage::new_stroke(&Stroke::new(4.)), List::new_from_element(stroke).into_paint(), stroke_placement);
appearance appearance
}; };