diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index 7f8941df2f..9a877f06d6 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -2774,7 +2774,7 @@ impl DocumentMessageHandler { let has_fill = fill_graphic_list.is_some_and(|list| is_paint_present(list)); // `Vector.stroke` captures stroke geometry, even with weight 0 or transparent paint. - // So stroke visibility must be checked from `ATTR_STROKE`, the paint source of truth. + // So stroke visibility must be checked from the stroke paint snapshot, the paint source of truth. let stroke_visible = stroke_graphic_list.is_some_and(|list| list.element(0).is_some_and(|g| !g.is_fully_transparent())); let has_stroke = stroke.as_ref().is_some_and(|s| s.has_renderable_stroke()) && stroke_visible; diff --git a/editor/src/messages/portfolio/document/utility_types/document_metadata.rs b/editor/src/messages/portfolio/document/utility_types/document_metadata.rs index 67bdf7624e..eb7f4ebf86 100644 --- a/editor/src/messages/portfolio/document/utility_types/document_metadata.rs +++ b/editor/src/messages/portfolio/document/utility_types/document_metadata.rs @@ -41,10 +41,10 @@ pub struct DocumentMetadata { /// Vector data keyed by layer ID, used as fallback when no Path node exists. /// This provides accurate SegmentIds for layers without explicit Path nodes. pub layer_vector_data: HashMap>, - /// Per-layer `ATTR_FILL` attribute, exposed so message handlers can read paint + /// Per-layer fill paint snapshot, exposed so message handlers can read paint /// information that lives on the list. pub layer_fill_attributes: HashMap>>>, - /// Per-layer `ATTR_STROKE` attribute, exposed so message handlers can read + /// Per-layer stroke paint snapshot, exposed so message handlers can read /// stroke paint information that lives on the list. pub layer_stroke_attributes: HashMap>>>, /// Transform from document space to viewport space. diff --git a/editor/src/messages/portfolio/document/utility_types/network_interface.rs b/editor/src/messages/portfolio/document/utility_types/network_interface.rs index fca7ddf548..61006afd49 100644 --- a/editor/src/messages/portfolio/document/utility_types/network_interface.rs +++ b/editor/src/messages/portfolio/document/utility_types/network_interface.rs @@ -3439,12 +3439,12 @@ impl NodeNetworkInterface { self.document_metadata.layer_vector_data = new_layer_vector_data; } - /// Update the per-layer `ATTR_FILL` snapshot. + /// Update the per-layer fill paint snapshot. pub fn update_fill_attributes(&mut self, new_layer_fill_attributes: HashMap>>>) { self.document_metadata.layer_fill_attributes = new_layer_fill_attributes; } - /// Update the per-layer `ATTR_STROKE` snapshot. + /// Update the per-layer stroke paint snapshot. pub fn update_stroke_attributes(&mut self, new_layer_stroke_attributes: HashMap>>>) { self.document_metadata.layer_stroke_attributes = new_layer_stroke_attributes; } diff --git a/node-graph/libraries/graphic-types/src/graphic/glue.rs b/node-graph/libraries/graphic-types/src/graphic/glue.rs index f5145294db..83ca9a9a83 100644 --- a/node-graph/libraries/graphic-types/src/graphic/glue.rs +++ b/node-graph/libraries/graphic-types/src/graphic/glue.rs @@ -492,7 +492,7 @@ mod run_tests { use super::*; use crate::graphic::test_support::{native_group_paint, unit_square_at}; use crate::graphic::{group_to_legacy_list, map_groups_to_legacy}; - use crate::markers::{Fill, Stroke}; + use crate::markers::EditorMergedLayers; use core_types::attribute::Attribute; use core_types::lane::LaneSource; use core_types::record::{FieldWrite, RunBuilder, RunView, element_write_hashed}; @@ -504,9 +504,9 @@ mod run_tests { let vector = unit_square_at(DVec2::ZERO); let source = core_types::arena::Arena::new(1 << 16).unwrap(); - let mut builder = RunBuilder::new(&source, element_write_hashed::(), &[FieldWrite::of::(0)], 1).unwrap(); + let mut builder = RunBuilder::new(&source, element_write_hashed::(), &[FieldWrite::of::(0)], 1).unwrap(); let lane = builder.push(vector.clone()).unwrap(); - builder.attr::(lane, Some(&paint)); + builder.attr::(lane, Some(&paint)); let group = core_types::record::Group { row: None, content: builder.finish() }; let expected = group_to_legacy_list(&group); let owned = map_groups_to_owned(&Graphic::Group(group)); @@ -548,9 +548,9 @@ mod run_tests { let paint = unsafe { core_types::record::erase_static(native_group_paint(&inner_vector, &source)) }; let vector = unit_square_at(DVec2::new(4., 4.)); - let mut builder = RunBuilder::new(&source, element_write_hashed::(), &[FieldWrite::of::(0)], 1).unwrap(); + let mut builder = RunBuilder::new(&source, element_write_hashed::(), &[FieldWrite::of::(0)], 1).unwrap(); let lane = builder.push(vector.clone()).unwrap(); - builder.attr::(lane, Some(&paint)); + builder.attr::(lane, Some(&paint)); let item = builder.finish(); let owned = item.copy_out(); let expected = map_groups_to_legacy(paint.element(0).unwrap()); @@ -561,7 +561,7 @@ mod run_tests { let arena = core_types::arena::Arena::new(1 << 16).unwrap(); let replayed = owned.replay(&arena).expect("the arena holds the replay"); let run = RunView::::new(&replayed).expect("the run holds vector elements"); - let served = run.attr::(0).expect("the fill replays present"); + let served = run.attr::(0).expect("the fill replays present"); assert_eq!(map_groups_to_legacy(served.element(0).unwrap()), expected); } @@ -574,12 +574,12 @@ mod run_tests { let paint = unsafe { core_types::record::erase_static(native_group_paint(&inner_vector, &source)) }; let vector = unit_square_at(DVec2::new(4., 4.)); - let mut builder = RunBuilder::new(&source, element_write_hashed::(), &[FieldWrite::of::(0)], 1).unwrap(); + let mut builder = RunBuilder::new(&source, element_write_hashed::(), &[FieldWrite::of::(0)], 1).unwrap(); let lane = builder.push(vector.clone()).unwrap(); - builder.attr::(lane, Some(&paint)); + builder.attr::(lane, Some(&paint)); let item = builder.finish(); let layout = item.layout().clone(); - let offset = layout.offset_of(Fill::NAME, 0).unwrap(); + let offset = layout.offset_of(EditorMergedLayers::NAME, 0).unwrap(); // SAFETY: the item's lane is a live record of `layout`. let owned = unsafe { core_types::record::OwnedRecord::copy_out(&layout, item.lanes().get(0).rec()) }; let expected = map_groups_to_legacy(paint.element(0).unwrap()); @@ -609,8 +609,8 @@ mod run_tests { ) -> (core_types::record::Layout, core_types::record::MaterializedSpan, Vec) { use core_types::record::{Layout, MaterializedSpan, Promotion, element_write, write_field}; - let layout = Layout::default().with_writes(0, element_write::(), &[FieldWrite::of::(0)]); - let offset = layout.offset_of(Fill::NAME, 0).unwrap(); + let layout = Layout::default().with_writes(0, element_write::(), &[FieldWrite::of::(0)]); + let offset = layout.offset_of(EditorMergedLayers::NAME, 0).unwrap(); let stride = layout.lane_stride(); let mut buffer = vec![0u64; (lanes * stride).div_ceil(8)]; let base = buffer.as_mut_ptr().cast::(); @@ -633,7 +633,7 @@ mod run_tests { /// The promoted paint of one lane, at the layout the promote published. fn promoted_paint<'p>(span: &core_types::record::MaterializedSpan, layout: &core_types::record::Layout, lane: usize, persistent: &'p core_types::arena::Arena) -> &'p List> { - let offset = layout.offset_of(Fill::NAME, 0).unwrap(); + let offset = layout.offset_of(EditorMergedLayers::NAME, 0).unwrap(); let batch = span.batch(persistent, layout).expect("the span resolves in its own region"); // SAFETY: the promote wrote a record of `layout` into every lane. unsafe { batch.get(lane).rec().read::>>(offset) }.expect("the paint promotes present") @@ -713,7 +713,7 @@ mod run_tests { let mut paint = List::new_from_element(Graphic::Vector(unit_square_at(DVec2::new(4., 4.)))); // SAFETY: the erased native list serves only while `transient` is live; the // promote under test replaces its borrows. - paint.set_attribute::>>(Stroke::NAME, 0, Some(unsafe { core_types::record::erase_static(native) })); + paint.set_attribute::>>("probe:paint", 0, Some(unsafe { core_types::record::erase_static(native) })); let (paint, _) = transient.alloc_sized_keyed(paint, 0).unwrap(); let (layout, span, _frames) = promote_paint_field(Some(paint), 1, &transient, &persistent); @@ -723,7 +723,7 @@ mod run_tests { transient.reset(); let served = promoted_paint(&span, &layout, 0, &persistent); - let held = served.attribute::>>(Stroke::NAME, 0).expect("the stroke attribute rides the promoted list"); + let held = served.attribute::>>("probe:paint", 0).expect("the stroke attribute rides the promoted list"); let held = held.as_ref().expect("the stroke is present"); assert_eq!( map_groups_to_legacy(held.element(0).unwrap()), @@ -742,7 +742,7 @@ mod run_tests { // whose value holds none: neither denies the move. paint.set_attribute::("opacity", 0, 0.5); paint.set_attribute::("probe:color", 0, Color::BLACK); - paint.set_attribute::>>(Stroke::NAME, 0, Some(List::new_from_element(Graphic::Color(Color::WHITE)))); + paint.set_attribute::>>("probe:paint", 0, Some(List::new_from_element(Graphic::Color(Color::WHITE)))); let heap = { let Some(Graphic::Vector(vector)) = paint.element(0) else { panic!("the paint carries a vector") }; vector.point_domain.positions().as_ptr() @@ -777,15 +777,15 @@ mod run_tests { let mut paint = List::new_from_element(Graphic::Vector(unit_square_at(DVec2::new(2., 2.)))); // SAFETY: the erased native list serves only while `source` is live; the // deep glue under test replaces its borrows at the copy-out seam. - paint.set_attribute::>>(Stroke::NAME, 0, Some(unsafe { core_types::record::erase_static(native) })); + paint.set_attribute::>>("probe:paint", 0, Some(unsafe { core_types::record::erase_static(native) })); let vector = unit_square_at(DVec2::new(4., 4.)); - let mut builder = RunBuilder::new(&source, element_write_hashed::(), &[FieldWrite::of::(0)], 1).unwrap(); + let mut builder = RunBuilder::new(&source, element_write_hashed::(), &[FieldWrite::of::(0)], 1).unwrap(); let lane = builder.push(vector.clone()).unwrap(); - builder.attr::(lane, Some(&paint)); + builder.attr::(lane, Some(&paint)); let item = builder.finish(); let layout = item.layout().clone(); - let offset = layout.offset_of(Fill::NAME, 0).unwrap(); + let offset = layout.offset_of(EditorMergedLayers::NAME, 0).unwrap(); // SAFETY: the item's lane is a live record of `layout`. let owned = unsafe { core_types::record::OwnedRecord::copy_out(&layout, item.lanes().get(0).rec()) }; drop(item); @@ -800,7 +800,7 @@ mod run_tests { let value = unsafe { slot.finish() }; // SAFETY: the replay wrote a record of `layout`. let served = unsafe { layout.rec(&value).read::>>(offset) }.expect("the fill replays present"); - let held = served.attribute::>>(Stroke::NAME, 0).expect("the stroke attribute rides the replayed list"); + let held = served.attribute::>>("probe:paint", 0).expect("the stroke attribute rides the replayed list"); let held = held.as_ref().expect("the stroke is present"); assert_eq!(map_groups_to_legacy(held.element(0).unwrap()), expected, "the attribute-held group replayed into the serving arena"); } diff --git a/node-graph/libraries/graphic-types/src/graphic/legacy.rs b/node-graph/libraries/graphic-types/src/graphic/legacy.rs index 50a2578153..f2bbda4e39 100644 --- a/node-graph/libraries/graphic-types/src/graphic/legacy.rs +++ b/node-graph/libraries/graphic-types/src/graphic/legacy.rs @@ -1,8 +1,8 @@ //! The legacy bridge: record-backed groups rebuilt as owned legacy lists. -use super::walk::push_lane_paint_into_interiors; use super::{Graphic, detable_items}; -use crate::markers::{ATTR_FILL, ATTR_STROKE}; +use crate::appearance::Appearance; +use crate::markers::{ATTR_APPEARANCE, ATTR_EDITOR_MERGED_LAYERS, ATTR_PAINT}; use core_types::Color; use core_types::list::{Item, List}; use raster_types::{CPU, GPU, Raster}; @@ -15,11 +15,19 @@ pub fn run_to_list(item: &cor core_types::record::run_to_owned_list(item) } -/// Converts the group content of the list's paint attribute values to legacy -/// form, so a legacy product owns everything its attributes reach. +/// Converts the group content the list's attribute values reach to legacy +/// form, so a legacy product owns everything its attributes hold: the paint +/// cells inside each appearance, and the merged-layers snapshot. pub fn map_paint_attrs_to_legacy(list: &mut List) { - for key in [ATTR_FILL, ATTR_STROKE, crate::markers::ATTR_EDITOR_MERGED_LAYERS] { - let Some(values) = list.iter_attribute_values_mut::>>(key) else { continue }; + if let Some(appearances) = list.iter_attribute_values_mut::(ATTR_APPEARANCE) { + for appearance in appearances { + let Some(cells) = appearance.0.iter_attribute_values_mut::(ATTR_PAINT) else { continue }; + for cell in cells { + *cell = map_groups_to_legacy(cell); + } + } + } + if let Some(values) = list.iter_attribute_values_mut::>>(ATTR_EDITOR_MERGED_LAYERS) { for value in values.flatten() { for element in value.iter_element_values_mut() { *element = map_groups_to_legacy(element); @@ -86,7 +94,6 @@ pub fn group_to_legacy_list(group: &core_types::record::Group) -> List(item).map(|list| detable_items(list, Graphic::Vector))) @@ -101,24 +108,25 @@ pub fn group_to_legacy_list(group: &core_types::record::Group) -> List(), &[FieldWrite::of::(0)], 1).unwrap(); + let mut builder = RunBuilder::new(&source, element_write_hashed::(), &[FieldWrite::of::(0)], 1).unwrap(); let lane = builder.push(vector.clone()).unwrap(); - builder.attr::(lane, Some(&paint)); + builder.attr::(lane, Some(&appearance)); let item = builder.finish(); let legacy = run_to_legacy_list::(&item).expect("the run lowers to a legacy vector list"); let expected = map_groups_to_legacy(paint.element(0).unwrap()); @@ -126,9 +134,10 @@ mod run_tests { drop(paint); drop(source); - let served = legacy.attribute::>>(Fill::NAME, 0).expect("the fill attribute rides the list"); - let served = served.as_ref().expect("the fill is present"); - assert_eq!(served.element(0).unwrap(), &expected); + let served = legacy.attribute::(ATTR_APPEARANCE, 0).expect("the appearance rides the list"); + let cell = served.paint_at(0).expect("the fill coverage keeps its paint"); + let Graphic::Graphic(cell_rows) = cell else { panic!("the paint cell keeps the list form") }; + assert_eq!(cell_rows.element(0).unwrap(), &expected); } #[test] diff --git a/node-graph/libraries/graphic-types/src/graphic/mod.rs b/node-graph/libraries/graphic-types/src/graphic/mod.rs index b6b53aa36f..4550c16292 100644 --- a/node-graph/libraries/graphic-types/src/graphic/mod.rs +++ b/node-graph/libraries/graphic-types/src/graphic/mod.rs @@ -7,10 +7,7 @@ pub(crate) use glue::{list_contains_groups, map_attribute_groups_to_owned, map_a pub use glue::{map_groups_to_owned, map_groups_to_persistent, map_groups_to_resident}; pub(crate) use legacy::run_to_legacy_list; pub use legacy::{group_to_legacy_graphic, group_to_legacy_list, map_groups_to_legacy, map_paint_attrs_to_legacy, run_to_list}; -pub use paint::{ - LanePaint, PaintColumns, PaintReach, bake_paint_transforms, has_paint, is_paint_present, paint_cell_rows, paint_graphics, set_paint_attribute, set_paint_attribute_at, - vector_can_reduce_to_clip_path, -}; +pub use paint::{PaintColumns, PaintReach, bake_paint_transforms, is_paint_present, paint_cell_rows, vector_can_reduce_to_clip_path}; pub use walk::{GraphicLevel, GraphicLevelColumn, RowStep, VectorRow, direct_vector_len, flatten_vector_rows, group_is_empty, lane_attributes, run_lane_attributes, walk_vector_rows}; use walk::{group_all_clipped, group_bounding_box, group_is_fully_transparent, group_is_opaque, group_render_complexity}; diff --git a/node-graph/libraries/graphic-types/src/graphic/paint.rs b/node-graph/libraries/graphic-types/src/graphic/paint.rs index ad9c8dfeab..25518c2b40 100644 --- a/node-graph/libraries/graphic-types/src/graphic/paint.rs +++ b/node-graph/libraries/graphic-types/src/graphic/paint.rs @@ -1,10 +1,10 @@ -//! The paint column level: fill and stroke read as lane columns and threaded down to the elements they reach. +//! The appearance cascade level: the declared appearance read as a lane column and threaded down to the elements it reaches. -use super::{Graphic, IntoGraphicList}; +use super::Graphic; use crate::appearance::Appearance; -use crate::markers::{ATTR_FILL, ATTR_STROKE, Appearance as AppearanceMarker, Fill, Stroke}; +use crate::markers::Appearance as AppearanceMarker; use core_types::ATTR_TRANSFORM; -use core_types::attribute::{Attribute, Opacity}; +use core_types::attribute::Opacity; use core_types::lane::{LaneColumn, LaneSource}; use core_types::list::{ItemAttributeValues, List}; use glam::DAffine2; @@ -16,28 +16,6 @@ pub fn is_paint_present(graphic_list: &List) -> bool { graphic_list.element(0).is_some_and(|graphic| !graphic.is_empty()) } -/// Look up the paint graphics stored under the marker `A`, in the canonical `List` form. -pub fn paint_graphics<'a, A, S>(source: &'a S, index: usize) -> Option<&'a List>> -where - S: LaneSource, - A: Attribute = Option<&'a List>>>, -{ - source - .attr::(index) - // Treat a blank paint attribute as absent so an empty attribute doesn't count as painted - .filter(|graphic_list| is_paint_present(graphic_list)) -} - -/// Whether the item carries a non-blank canonical `List` paint under the marker `A`, -/// checked by borrowing without cloning the renderable list. -pub fn has_paint<'a, A, S>(source: &'a S, index: usize) -> bool -where - S: LaneSource, - A: Attribute = Option<&'a List>>>, -{ - paint_graphics::(source, index).is_some() -} - /// Whether every lane of a vector source draws as a plain clip path: fully /// opaque, fill absent or opaque, stroke invisible or fully transparent. pub fn vector_can_reduce_to_clip_path>(source: &S, inherited_appearance: Option<&Appearance>) -> bool { @@ -65,116 +43,41 @@ pub fn vector_can_reduce_to_clip_path>(source: & }) } -/// The paint a lane carries for its interiors, in the reference form -/// [`PaintOverlay`] threads down. -#[derive(Clone, Copy, Default)] -pub struct LanePaint<'a> { - pub fill: Option<&'a List>>, - pub stroke: Option<&'a List>>, -} - -impl<'a> LanePaint<'a> { - pub const NONE: Self = Self { fill: None, stroke: None }; - - pub fn is_present(&self) -> bool { - self.fill.is_some() || self.stroke.is_some() - } -} - -/// A source's fill, stroke, and appearance columns, resolved once for per-lane reads. +/// A source's declared appearance column, resolved once for per-lane reads. pub struct PaintColumns<'a, S: LaneSource + 'a> { - fill: S::Column<'a, Fill>, - stroke: S::Column<'a, Stroke>, appearance: S::Column<'a, AppearanceMarker>, } impl<'a, S: LaneSource> PaintColumns<'a, S> { pub fn new(source: &'a S) -> Self { Self { - fill: source.column::(), - stroke: source.column::(), appearance: source.column::(), } } - /// The lane's present, non-blank paint. - pub fn read(&self, lane: usize) -> LanePaint<'a> { - let present = |value: Option>>>| value.flatten().filter(|list| is_paint_present(list)); - LanePaint { - fill: present(self.fill.try_get(lane)), - stroke: present(self.stroke.try_get(lane)), - } - } - /// The lane's own declared appearance; an absent or empty cell is undeclared. pub fn read_appearance(&self, lane: usize) -> Option<&'a Appearance> { self.appearance.try_get(lane).flatten().and_then(Appearance::declared) } } -/// How far a lane's paint reaches into the element beneath it, mirroring the -/// legacy conversion's paint push: vector interiors directly and vector -/// children of a nested graphic list, one level deep. -/// -/// The appearance cascade rides beside the paint push with its own rule: a -/// lane's own declared appearance wins wholesale, an undeclared lane inherits -/// the nearest ancestor's, at any depth. Only a fresh entry (a pattern's own +/// The appearance cascade threading down the graphic levels: a lane's own +/// declared appearance wins wholesale, an undeclared lane inherits the +/// nearest ancestor's, at any depth. Only a fresh entry (a pattern's own /// render, or any standalone render root) starts without an inherited one. #[derive(Clone, Copy)] pub struct PaintReach<'a> { - pub paint: LanePaint<'a>, /// The cascade's resolved appearance: the nearest declared one at or above this lane. pub appearance: Option<&'a Appearance>, - hops: u8, } impl<'a> PaintReach<'a> { - pub const NONE: Self = Self { - paint: LanePaint::NONE, - appearance: None, - hops: 0, - }; + pub const NONE: Self = Self { appearance: None }; - /// The lane's effective reach: an inherited paint stays authoritative - /// (lane paint below a push's origin is inert in the legacy model), an - /// absent one reads the lane's own paint. The appearance arbitrates the - /// opposite way: the lane's own declared appearance wins over the - /// inherited one. + /// The lane's effective reach: its own declared appearance wins over the inherited one. pub fn for_lane(self, columns: &PaintColumns<'a, S>, index: usize) -> Self { - let appearance = Appearance::cascade(columns.read_appearance(index), self.appearance); - match self.paint.is_present() { - true => Self { appearance, ..self }, - false => Self { - paint: columns.read(index), - appearance, - hops: 2, - }, - } - } - - pub fn applies(&self) -> bool { - self.hops > 0 && self.paint.is_present() - } - - /// The reach one graphic nesting level further down. The appearance - /// cascade is not hop-limited, so it passes through unchanged. - pub fn nested(self) -> Self { Self { - hops: self.hops.saturating_sub(1), - ..self - } - } - - /// The reach entering a group's own graphic run: a spent or absent reach - /// resets so the group's own lane paint applies at its own boundary, - /// while the appearance cascades through the boundary. - pub fn into_group_graphics(self) -> Self { - match self.applies() { - true => self.nested(), - false => Self { - appearance: self.appearance, - ..Self::NONE - }, + appearance: Appearance::cascade(columns.read_appearance(index), self.appearance), } } } @@ -189,18 +92,8 @@ pub fn paint_cell_rows<'a>(cell: &'a Graphic<'static>) -> Option<&'a List(list: &mut List, index: usize, key: &str, paint: impl IntoGraphicList) { - list.set_attribute(key, index, Some(paint.into_graphic_list())); -} - -/// Bake the provided transform into the per-item transforms of the paint graphics stored under the -/// canonical `List` fill and stroke attributes. +/// Bake the provided transform into the per-item transforms of the paint +/// graphics inside the item's appearance coverage cells. pub fn bake_paint_transforms(attributes: &mut ItemAttributeValues, transform: DAffine2) { fn bake_graphic_paint_transform(graphics: &mut List, transform: DAffine2) { for item_transform in graphics.iter_attribute_values_mut_or_default::(ATTR_TRANSFORM) { @@ -213,9 +106,13 @@ pub fn bake_paint_transforms(attributes: &mut ItemAttributeValues, transform: DA } } - for paint_key in [ATTR_FILL, ATTR_STROKE] { - if let Some(Some(graphics)) = attributes.get_mut::>>(paint_key) { - bake_graphic_paint_transform(graphics, transform); + if let Some(appearance) = attributes.get_mut::(crate::markers::ATTR_APPEARANCE) { + if let Some(cells) = appearance.0.iter_attribute_values_mut::(crate::markers::ATTR_PAINT) { + for cell in cells { + if let Graphic::Graphic(list) = cell { + bake_graphic_paint_transform(list, transform); + } + } } } } @@ -230,23 +127,24 @@ mod run_tests { use glam::DVec2; #[test] - fn a_run_serves_the_parked_paint_reference() { - let paint = List::new_from_element(Graphic::Color(Color::BLACK)); + fn a_run_serves_the_parked_appearance_reference() { + use crate::appearance::Coverage; + use core_types::lane::LaneSource; + + let appearance = Appearance::new_single(Coverage::new_fill(), Graphic::Color(Color::BLACK)); let vector = unit_square_at(DVec2::ZERO); let arena = core_types::arena::Arena::new(1 << 16).unwrap(); - let mut builder = RunBuilder::new(&arena, element_write_hashed::(), &[FieldWrite::of::(0)], 1).unwrap(); + let mut builder = RunBuilder::new(&arena, element_write_hashed::(), &[FieldWrite::of::(0)], 1).unwrap(); let lane = builder.push(vector.clone()).unwrap(); - builder.attr::(lane, Some(&paint)); + builder.attr::(lane, Some(&appearance)); let item = builder.finish(); let run = RunView::::new(&item).expect("the run holds vector elements"); - assert_eq!(run.attr::(0), Some(&paint)); - assert_eq!(paint_graphics::(&run, 0), Some(&paint)); - assert_eq!(paint_graphics::(&run, 0), None); + assert_eq!(run.attr::(0), Some(&appearance)); let legacy = run_to_legacy_list::(&item).expect("the run lowers to a legacy vector list"); - assert_eq!(paint_graphics::(&legacy, 0), paint_graphics::(&run, 0)); + assert_eq!(legacy.attr::(0), Some(&appearance)); } #[test] @@ -263,32 +161,10 @@ mod run_tests { list.set_attribute(ATTR_APPEARANCE, 0, own.clone()); let columns = PaintColumns::new(&list); - let ancestor = PaintReach { - appearance: Some(&inherited), - ..PaintReach::NONE - }; + let ancestor = PaintReach { appearance: Some(&inherited) }; assert_eq!(ancestor.for_lane(&columns, 0).appearance, Some(&own), "a declared lane wins over the inherited appearance"); assert_eq!(ancestor.for_lane(&columns, 1).appearance, Some(&inherited), "a padded lane inherits"); assert_eq!(PaintReach::NONE.for_lane(&columns, 1).appearance, None, "no ancestor leaves an undeclared lane bare"); } - - #[test] - fn reach_carries_the_appearance_through_nesting_and_group_boundaries() { - use crate::appearance::Coverage; - - let inherited = Appearance::new_single(Coverage::new_fill(), Graphic::Color(Color::WHITE)); - let reach = PaintReach { - appearance: Some(&inherited), - ..PaintReach::NONE - }; - - assert_eq!(reach.nested().appearance, Some(&inherited), "nesting does not hop-limit the cascade"); - assert_eq!( - reach.into_group_graphics().appearance, - Some(&inherited), - "the cascade crosses a group boundary the paint push resets at" - ); - assert_eq!(PaintReach::NONE.into_group_graphics().appearance, None); - } } diff --git a/node-graph/libraries/graphic-types/src/graphic/walk.rs b/node-graph/libraries/graphic-types/src/graphic/walk.rs index 419ad665df..b38c6d901c 100644 --- a/node-graph/libraries/graphic-types/src/graphic/walk.rs +++ b/node-graph/libraries/graphic-types/src/graphic/walk.rs @@ -1,9 +1,9 @@ //! The native-content walk: vector rows reached through a graphic's own storage, with no legacy conversion. use super::Graphic; -use super::paint::{LanePaint, PaintColumns, PaintReach, is_paint_present, paint_graphics, set_paint_attribute_at}; +use super::paint::{PaintColumns, PaintReach}; use crate::appearance::Appearance; -use crate::markers::{ATTR_APPEARANCE, ATTR_FILL, ATTR_STROKE, Fill}; +use crate::markers::ATTR_APPEARANCE; use core_types::attribute::{Attribute, ClippingMask, EditorLayerPath, Opacity, OpacityFill, Transform}; use core_types::bounds::{BoundingBox, RenderBoundingBox}; use core_types::lane::LaneSource; @@ -252,7 +252,6 @@ pub struct VectorRow<'w> { source: RowSourceRef<'w>, scale: FlattenScale, layer_path: Option<&'w [NodeId]>, - paint: LanePaint<'w>, appearance: Option<&'w Appearance>, } @@ -272,16 +271,14 @@ impl VectorRow<'_> { } } - /// Whether the built row will carry fill paint: the reaching lane paint, - /// else the row's own. + /// Whether the built row will carry fill paint: a painted fill coverage on + /// the row's resolved appearance. pub fn has_fill(&self) -> bool { - if self.paint.fill.is_some() { - return true; - } - match &self.source { - RowSourceRef::Lane(level, index) => paint_graphics::(level, *index).is_some(), - RowSourceRef::Run(run, _, index) => paint_graphics::(*run, *index).is_some(), - } + let own = match &self.source { + RowSourceRef::Lane(level, index) => level.attr::(*index), + RowSourceRef::Run(run, _, index) => LaneSource::attr::(*run, *index), + }; + Appearance::cascade(own, self.appearance).is_some_and(|appearance| appearance.has_painted_cover(crate::appearance::Cover::Fill)) } /// Builds the row at the end of `out`, applying the reach paint and the @@ -298,11 +295,6 @@ impl VectorRow<'_> { out.push(Item::from_parts(vector, run_lane_attributes(item, *lane))); } } - for (key, slot) in [(ATTR_FILL, self.paint.fill), (ATTR_STROKE, self.paint.stroke)] { - if let Some(paint) = slot { - set_paint_attribute_at(out, index, key, paint.clone()); - } - } if self.scale.has_transform || out.attribute::(ATTR_TRANSFORM, index).is_some() { let row_transform: DAffine2 = out.attribute_cloned_or_default(ATTR_TRANSFORM, index); out.set_attribute(ATTR_TRANSFORM, index, self.scale.transform * row_transform); @@ -331,7 +323,6 @@ fn walk_rows_of_run( item: &core_types::record::GroupItem, scale: FlattenScale, layer_path: Option<&[NodeId]>, - paint: LanePaint<'_>, appearance: Option<&Appearance>, visit: &mut dyn FnMut(VectorRow<'_>) -> RowStep, ) -> RowStep { @@ -343,7 +334,6 @@ fn walk_rows_of_run( source: RowSourceRef::Run(&run, item, lane), scale, layer_path, - paint, appearance, }) { return RowStep::Stop; @@ -353,9 +343,9 @@ fn walk_rows_of_run( } /// Walks a graphic level into its flattened vector rows, matching the legacy -/// push-then-flatten lowering: lane paint threads with [`PaintReach`], -/// ancestor transform, opacity and fill opacity compose down, the containing -/// level's parent layer path overwrites its rows, and non-vector content is +/// flatten lowering: the appearance cascades with [`PaintReach`], ancestor +/// transform, opacity and fill opacity compose down, the containing level's +/// parent layer path overwrites its rows, and non-vector content is /// discarded. A de-tabled leaf's row is its lane, attributes included. pub fn walk_vector_rows(level: GraphicLevel<'_>, visit: &mut dyn FnMut(VectorRow<'_>) -> RowStep) { walk_vector_rows_impl(level, FlattenScale::ROOT, None, PaintReach::NONE, visit); @@ -371,48 +361,27 @@ fn walk_vector_rows_impl<'a>( if let GraphicLevel::Run(item) = level { // A vector-typed run is already its rows. if item.typed_lanes::().is_some() { - let paint = match inherited.applies() { - true => inherited.paint, - false => LanePaint::NONE, - }; - return walk_rows_of_run(item, scale, parent_layer_path, paint, inherited.appearance, visit); + return walk_rows_of_run(item, scale, parent_layer_path, inherited.appearance, visit); } } let columns = PaintColumns::new(&level); for index in 0..level.lane_count() { let Some(element) = level.element(index) else { continue }; let reach = inherited.for_lane(&columns, index); - let row_paint = match reach.applies() { - true => reach.paint, - false => LanePaint::NONE, - }; let step = match element { Graphic::Vector(_) => visit(VectorRow { source: RowSourceRef::Lane(level, index), scale, layer_path: parent_layer_path, - paint: row_paint, appearance: reach.appearance, }), - Graphic::Graphic(children) => walk_vector_rows_impl( - GraphicLevel::Legacy(children), - scale.composed(&level, index), - level.try_attr::(index), - reach.nested(), - visit, - ), + Graphic::Graphic(children) => walk_vector_rows_impl(GraphicLevel::Legacy(children), scale.composed(&level, index), level.try_attr::(index), reach, visit), Graphic::Group(group) => { let item = &group.content; if item.typed_lanes::().is_some() { - walk_rows_of_run(item, scale.composed(&level, index), level.try_attr::(index), row_paint, reach.appearance, visit) + walk_rows_of_run(item, scale.composed(&level, index), level.try_attr::(index), reach.appearance, visit) } else if item.typed_lanes::().is_some() { - walk_vector_rows_impl( - GraphicLevel::Run(item), - scale.composed(&level, index), - level.try_attr::(index), - reach.into_group_graphics(), - visit, - ) + walk_vector_rows_impl(GraphicLevel::Run(item), scale.composed(&level, index), level.try_attr::(index), reach, visit) } else { RowStep::Continue } @@ -437,26 +406,6 @@ pub fn flatten_vector_rows(level: GraphicLevel<'_>) -> List { out } -/// The transitional paint placement: a lane-level fill or stroke paint -/// attribute moves onto the vector interiors the legacy paint readers -/// inspect, reaching as far as the pre-flip broadcast did. -pub(in crate::graphic) fn push_lane_paint_into_interiors(list: &mut List) { - for index in 0..list.len() { - for key in [ATTR_FILL, ATTR_STROKE] { - let stored = list.attribute::>>(key, index).and_then(|optional| optional.as_ref()); - let Some(paint) = stored.filter(|paint| is_paint_present(paint)).cloned() else { - continue; - }; - let Some(Graphic::Graphic(children)) = list.element_mut(index) else { continue }; - for child in 0..children.len() { - if matches!(children.element(child), Some(Graphic::Vector(_))) { - set_paint_attribute_at(children, child, key, paint.clone()); - } - } - } - } -} - /// The count [`map_groups_to_legacy`] would expose through [`Graphic::as_vector`], /// read from the run's lanes instead of materializing the legacy list. Mirrors /// [`group_to_legacy_graphic`]'s typed-run path, where `Vector` is tried first. @@ -496,6 +445,10 @@ mod run_tests { #[test] fn the_vector_row_walk_matches_the_legacy_flatten() { + use crate::appearance::Coverage; + + let single = |color: Color| Appearance::new_single(Coverage::new_fill(), Graphic::Color(color)); + let inner_vector = unit_square_at(DVec2::ZERO); let arena = core_types::arena::Arena::new(1 << 16).unwrap(); let mut builder = RunBuilder::new(&arena, element_write_hashed::(), &[], 1).unwrap(); @@ -507,7 +460,7 @@ mod run_tests { painted.push(Item::new_from_element(Graphic::Vector(unit_square_at(DVec2::ONE)))); painted.set_attribute(core_types::ATTR_TRANSFORM, 0, DAffine2::from_translation(DVec2::new(1., 0.))); painted.set_attribute(core_types::ATTR_TRANSFORM, 1, DAffine2::from_translation(DVec2::new(0., 1.))); - set_paint_attribute_at(&mut painted, 1, ATTR_FILL, List::new_from_element(Graphic::Color(Color::WHITE))); + painted.set_attribute(ATTR_APPEARANCE, 1, single(Color::WHITE)); let mut nested = List::new_from_element(Graphic::Vector(unit_square_at(DVec2::new(2., 2.)))); nested.set_attribute(core_types::ATTR_TRANSFORM, 0, DAffine2::from_scale(DVec2::splat(2.))); @@ -520,12 +473,12 @@ mod run_tests { top.push(Item::new_from_element(Graphic::Vector(unit_square_at(DVec2::new(6., 0.))))); top.set_attribute(core_types::ATTR_TRANSFORM, 0, DAffine2::from_translation(DVec2::new(5., 5.))); top.set_attribute(core_types::ATTR_EDITOR_LAYER_PATH, 0, vec![core_types::uuid::NodeId(7)]); - set_paint_attribute_at(&mut top, 0, ATTR_FILL, List::new_from_element(Graphic::Color(Color::BLACK))); + top.set_attribute(ATTR_APPEARANCE, 0, single(Color::BLACK)); top.set_attribute(core_types::ATTR_OPACITY, 1, 0.5); top.set_attribute(core_types::ATTR_TRANSFORM, 2, DAffine2::from_scale(DVec2::splat(3.))); top.set_attribute(core_types::ATTR_TRANSFORM, 4, DAffine2::from_translation(DVec2::new(0., 7.))); top.set_attribute(core_types::ATTR_EDITOR_LAYER_PATH, 4, vec![core_types::uuid::NodeId(9)]); - set_paint_attribute_at(&mut top, 4, ATTR_FILL, List::new_from_element(Graphic::Color(Color::WHITE))); + top.set_attribute(ATTR_APPEARANCE, 4, single(Color::WHITE)); let legacy = { let mut list = List::new(); @@ -533,7 +486,6 @@ mod run_tests { let (element, attributes) = item.into_parts(); list.push(Item::from_parts(map_groups_to_legacy(&element), attributes)); } - push_lane_paint_into_interiors(&mut list); list.into_flattened_list::() }; let native = flatten_vector_rows(GraphicLevel::Legacy(&top)); @@ -555,9 +507,9 @@ mod run_tests { "layer path, row {row}" ); assert_eq!( - native.attribute::>>(ATTR_FILL, row), - legacy.attribute::>>(ATTR_FILL, row), - "fill, row {row}" + native.attribute::(ATTR_APPEARANCE, row), + legacy.attribute::(ATTR_APPEARANCE, row), + "appearance, row {row}" ); } assert_eq!(native, legacy); diff --git a/node-graph/libraries/graphic-types/src/lib.rs b/node-graph/libraries/graphic-types/src/lib.rs index 560a7e417b..104fdfb2e7 100644 --- a/node-graph/libraries/graphic-types/src/lib.rs +++ b/node-graph/libraries/graphic-types/src/lib.rs @@ -13,7 +13,7 @@ pub use vector_types; pub use appearance::{Appearance, Cover, CoverPlacement, Coverage, FillAndStroke, stamp_coverage}; pub use artboard::Artboard; pub use graphic::{Graphic, IntoGraphicList, TryFromGraphic, Vector}; -pub use markers::{ATTR_APPEARANCE, ATTR_EDITOR_MERGED_LAYERS, ATTR_FILL, ATTR_PAINT, ATTR_STROKE}; +pub use markers::{ATTR_APPEARANCE, ATTR_EDITOR_MERGED_LAYERS, ATTR_PAINT}; pub mod migrations { use crate::Vector; diff --git a/node-graph/libraries/graphic-types/src/markers.rs b/node-graph/libraries/graphic-types/src/markers.rs index b8473c0d9b..7c89701d00 100644 --- a/node-graph/libraries/graphic-types/src/markers.rs +++ b/node-graph/libraries/graphic-types/src/markers.rs @@ -1,21 +1,15 @@ //! Attribute markers whose value types live in this crate, with their name //! constants for the string-keyed legacy readers and writers. //! -//! The list-valued markers may carry native [`Graphic::Group`] content: the -//! registered deep field glue owns it across persistence seams, and legacy -//! products convert it through [`crate::graphic::map_paint_attrs_to_legacy`]. +//! The markers may carry native [`Graphic::Group`] content: the registered +//! deep field glue owns it across persistence seams, and legacy products +//! convert it through [`crate::graphic::map_paint_attrs_to_legacy`]. use crate::Graphic; use core_types::attribute::Attribute; use core_types::list::List; core_types::attribute! { - /// Vector graphics object's filled area paint, a graphic list in the canonical paint form. - /// An absent value means no fill. - pub Fill("fill"): Option<&List>>; - /// Vector graphics object's stroke paint, a graphic list in the canonical paint form. - /// An absent value means no stroke paint. - pub Stroke("stroke"): Option<&List>>; /// Snapshot of the upstream content that fed into a destructive merge (Boolean Operation, /// Rasterize, etc.), so the editor can still surface click targets for the original child /// layers after their content has been collapsed. @@ -102,8 +96,6 @@ unsafe impl Attribute for Paint { core_types::attribute!(@register Paint); -pub const ATTR_FILL: &str = Fill::NAME; -pub const ATTR_STROKE: &str = Stroke::NAME; pub const ATTR_EDITOR_MERGED_LAYERS: &str = EditorMergedLayers::NAME; pub const ATTR_APPEARANCE: &str = Appearance::NAME; pub const ATTR_PAINT: &str = Paint::NAME; @@ -116,27 +108,13 @@ mod tests { #[test] fn the_census_carries_this_crates_names() { - for name in ["fill", "stroke", "editor:merged_layers"] { - assert_eq!(info(name).unwrap().value_type, TypeId::of::>>()); - } + assert_eq!(info("editor:merged_layers").unwrap().value_type, TypeId::of::>>()); assert_eq!(info("appearance").unwrap().value_type, TypeId::of::>()); assert_eq!(info("paint").unwrap().value_type, TypeId::of::>()); } #[test] fn an_absent_paint_defaults_to_none() { - assert_eq!(::default(), None); - } - - #[test] - fn a_paint_marker_reads_back_what_the_paint_writer_stored() { - use core_types::lane::LaneSource; - - let paint = List::new_from_element(Graphic::default()); - let mut list = List::new_from_element(Graphic::default()); - crate::graphic::set_paint_attribute_at(&mut list, 0, ATTR_FILL, paint.clone()); - - assert_eq!(list.attr::(0), Some(&paint)); - assert_eq!(list.attr::(0), None); + assert_eq!(::default(), None); } } diff --git a/node-graph/libraries/rendering/src/renderer.rs b/node-graph/libraries/rendering/src/renderer.rs index cc75d98283..d05a5fb36d 100644 --- a/node-graph/libraries/rendering/src/renderer.rs +++ b/node-graph/libraries/rendering/src/renderer.rs @@ -24,7 +24,7 @@ use glam::{DAffine2, DMat2, DVec2}; use graphene_hash::CacheHashWrapper; use graphene_resource::Resource; use graphic_types::appearance::{Appearance, Coverage}; -use graphic_types::graphic::{PaintColumns, PaintReach, is_paint_present, paint_cell_rows, set_paint_attribute, vector_can_reduce_to_clip_path}; +use graphic_types::graphic::{PaintColumns, PaintReach, is_paint_present, paint_cell_rows, vector_can_reduce_to_clip_path}; use graphic_types::markers::{Appearance as AppearanceMarker, EditorMergedLayers}; use graphic_types::raster_types::{BitmapMut, CPU, GPU, Image, Raster, Texture}; use graphic_types::vector_types::gradient::{GradientStops, GradientType}; @@ -32,7 +32,7 @@ use graphic_types::vector_types::markers::{GradientType as GradientTypeAttr, Spr use graphic_types::vector_types::subpath::Subpath; use graphic_types::vector_types::vector::click_target::{ClickTarget, FreePoint}; use graphic_types::vector_types::vector::style::{RenderMode, StrokeAlign, StrokeCap, StrokeJoin}; -use graphic_types::{ATTR_FILL, Artboard, Graphic, Vector}; +use graphic_types::{Artboard, Graphic, Vector}; use kurbo::{Affine, BezPath, Cap, Join, Shape, StrokeOpts}; use num_traits::Zero; use skrifa::instance::{LocationRef, NormalizedCoord, Size}; @@ -540,10 +540,10 @@ pub struct RenderMetadata { pub text_frames: HashMap, pub clip_targets: HashSet, pub vector_data: HashMap>, - /// Per-layer `ATTR_FILL` row attribute, exposed so message handlers can read it. + /// Per-layer fill paint snapshot from the resolved appearance, exposed so message handlers can read it. #[cfg_attr(feature = "serde", serde(skip))] pub fill_attributes: HashMap>>>, - /// Per-layer `ATTR_STROKE` row attribute, exposed so message handlers can read it. + /// Per-layer stroke paint snapshot from the resolved appearance, exposed so message handlers can read it. #[cfg_attr(feature = "serde", serde(skip))] pub stroke_attributes: HashMap>>>, pub backgrounds: Vec, @@ -684,7 +684,7 @@ impl Render for Graphic<'_> { fn render_element_svg<'a>(element: &'a Graphic, reach: PaintReach<'a>, render: &mut SvgRender, render_params: &RenderParams) { match element { Graphic::Vector(vector) => render_vector_svg(&Single(vector), reach.appearance, render, render_params), - Graphic::Graphic(inner) => render_graphic_svg_with(inner, reach.nested(), render, render_params), + Graphic::Graphic(inner) => render_graphic_svg_with(inner, reach, render, render_params), Graphic::Group(group) => render_group_svg(group, reach, render, render_params), _ => element.render_svg(render, render_params), } @@ -693,7 +693,7 @@ fn render_element_svg<'a>(element: &'a Graphic, reach: PaintReach<'a>, render: & fn render_element_vello<'a>(element: &'a Graphic, reach: PaintReach<'a>, scene: &mut Scene, transform: DAffine2, context: &mut RenderContext, render_params: &RenderParams) { match element { Graphic::Vector(vector) => render_vector_vello(&Single(vector), reach.appearance, scene, transform, context, render_params), - Graphic::Graphic(inner) => render_graphic_vello_with(inner, reach.nested(), scene, transform, context, render_params), + Graphic::Graphic(inner) => render_graphic_vello_with(inner, reach, scene, transform, context, render_params), Graphic::Group(group) => render_group_vello(group, reach, scene, transform, context, render_params), _ => element.render_to_vello(scene, transform, context, render_params), } @@ -736,7 +736,7 @@ fn collect_element_metadata<'a>( } match element { - Graphic::Graphic(list) => collect_graphic_metadata_with(list, reach.nested(), metadata, footprint, element_id), + Graphic::Graphic(list) => collect_graphic_metadata_with(list, reach, metadata, footprint, element_id), Graphic::Vector(vector) => collect_vector_metadata(&Single(vector), reach.appearance, metadata, footprint, element_id), Graphic::RasterCPU(raster) => collect_raster_metadata(&Single(raster), metadata, footprint, element_id), Graphic::RasterGPU(raster) => collect_raster_metadata(&Single(raster), metadata, footprint, element_id), @@ -778,7 +778,7 @@ fn collect_group_row_metadata(group: &Group, metadata: &mut RenderMetadata, elem fn add_element_upstream_click_targets<'a>(element: &'a Graphic, reach: PaintReach<'a>, click_targets: &mut Vec) { match element { - Graphic::Graphic(list) => add_graphic_upstream_click_targets_with(list, reach.nested(), click_targets), + Graphic::Graphic(list) => add_graphic_upstream_click_targets_with(list, reach, click_targets), Graphic::Vector(vector) => add_vector_upstream_click_targets(&Single(vector), reach.appearance, click_targets), Graphic::RasterCPU(_) | Graphic::RasterGPU(_) => add_raster_upstream_click_targets(click_targets), Graphic::Color(_) | Graphic::Gradient(_) => {} @@ -789,7 +789,7 @@ fn add_element_upstream_click_targets<'a>(element: &'a Graphic, reach: PaintReac fn add_element_upstream_outline_targets<'a>(element: &'a Graphic, reach: PaintReach<'a>, outlines: &mut Vec) { match element { - Graphic::Graphic(list) => add_graphic_upstream_outline_targets_with(list, reach.nested(), outlines), + Graphic::Graphic(list) => add_graphic_upstream_outline_targets_with(list, reach, outlines), Graphic::Vector(vector) => add_vector_upstream_outline_targets(&Single(vector), reach.appearance, outlines), Graphic::RasterCPU(_) | Graphic::RasterGPU(_) => add_raster_upstream_click_targets(outlines), Graphic::Color(_) | Graphic::Gradient(_) => {} @@ -803,7 +803,7 @@ fn add_element_upstream_outline_targets<'a>(element: &'a Graphic, reach: PaintRe fn render_group_svg<'a>(group: &'a Group, reach: PaintReach<'a>, render: &mut SvgRender, render_params: &RenderParams) { let item = &group.content; if let Some(run) = RunView::::new(item) { - render_graphic_svg_with(&run, reach.into_group_graphics(), render, render_params) + render_graphic_svg_with(&run, reach, render, render_params) } else if let Some(run) = RunView::::new(item) { render_vector_svg(&run, reach.appearance, render, render_params) } else if let Some(run) = RunView::>::new(item) { @@ -821,7 +821,7 @@ fn render_group_svg<'a>(group: &'a Group, reach: PaintReach<'a>, render: &mut Sv fn render_group_vello<'a>(group: &'a Group, reach: PaintReach<'a>, scene: &mut Scene, transform: DAffine2, context: &mut RenderContext, render_params: &RenderParams) { let item = &group.content; if let Some(run) = RunView::::new(item) { - render_graphic_vello_with(&run, reach.into_group_graphics(), scene, transform, context, render_params) + render_graphic_vello_with(&run, reach, scene, transform, context, render_params) } else if let Some(run) = RunView::::new(item) { render_vector_vello(&run, reach.appearance, scene, transform, context, render_params) } else if let Some(run) = RunView::>::new(item) { @@ -843,7 +843,7 @@ fn render_group_vello<'a>(group: &'a Group, reach: PaintReach<'a>, scene: &mut S fn collect_group_metadata<'a>(group: &'a Group, reach: PaintReach<'a>, metadata: &mut RenderMetadata, footprint: Footprint, element_id: Option) { let item = &group.content; if let Some(run) = RunView::::new(item) { - collect_graphic_metadata_with(&run, reach.into_group_graphics(), metadata, footprint, element_id) + collect_graphic_metadata_with(&run, reach, metadata, footprint, element_id) } else if let Some(run) = RunView::::new(item) { collect_vector_metadata(&run, reach.appearance, metadata, footprint, element_id) } else if let Some(run) = RunView::>::new(item) { @@ -859,7 +859,7 @@ fn collect_group_metadata<'a>(group: &'a Group, reach: PaintReach<'a>, metadata: fn add_group_upstream_click_targets<'a>(group: &'a Group, reach: PaintReach<'a>, click_targets: &mut Vec) { let item = &group.content; if let Some(run) = RunView::::new(item) { - add_graphic_upstream_click_targets_with(&run, reach.into_group_graphics(), click_targets) + add_graphic_upstream_click_targets_with(&run, reach, click_targets) } else if let Some(run) = RunView::::new(item) { add_vector_upstream_click_targets(&run, reach.appearance, click_targets) } else if item.typed_lanes::>().is_some() || item.typed_lanes::>().is_some() { @@ -872,7 +872,7 @@ fn add_group_upstream_click_targets<'a>(group: &'a Group, reach: PaintReach<'a>, fn add_group_upstream_outline_targets<'a>(group: &'a Group, reach: PaintReach<'a>, outlines: &mut Vec) { let item = &group.content; if let Some(run) = RunView::::new(item) { - add_graphic_upstream_outline_targets_with(&run, reach.into_group_graphics(), outlines) + add_graphic_upstream_outline_targets_with(&run, reach, outlines) } else if let Some(run) = RunView::::new(item) { add_vector_upstream_outline_targets(&run, reach.appearance, outlines) } else if item.typed_lanes::>().is_some() || item.typed_lanes::>().is_some() { @@ -1410,7 +1410,6 @@ fn render_vector_svg>(source: &S, inherited_appe // 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); - set_paint_attribute(mask_item.attributes_mut(), ATTR_FILL, List::new_from_element(Color::BLACK)); mask_item.set_attribute(graphic_types::ATTR_APPEARANCE, black_fill_appearance()); let vector_item = List::new_from_item(mask_item); @@ -1766,7 +1765,6 @@ fn render_vector_vello>( // 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); - set_paint_attribute(mask_item.attributes_mut(), ATTR_FILL, List::new_from_element(Color::BLACK)); mask_item.set_attribute(graphic_types::ATTR_APPEARANCE, black_fill_appearance()); let vector_list = List::new_from_item(mask_item); @@ -3129,7 +3127,6 @@ impl SvgRenderAttrs<'_> { mod group_walk_tests { use super::*; use core_types::record::{FieldWrite, RunBuilder, element_write_hashed}; - use graphic_types::markers::Fill; use graphic_types::vector_types::vector::PointId; fn unit_square_at(corner: DVec2) -> Vector { @@ -3140,7 +3137,7 @@ mod group_walk_tests { List::new_from_element(Graphic::Color(Color::from_rgbaf32(0.8, 0.2, 0.33, 1.).unwrap())) } - /// The appearance the fill node stamps beside the legacy fill marker, so test content mirrors node output. + /// The appearance the fill node stamps, so test content mirrors node output. fn fill_appearance(paint: &List>) -> Appearance { Appearance::new_single(Coverage::new_fill(), Graphic::Graphic(paint.clone())) } @@ -3158,9 +3155,8 @@ mod group_walk_tests { let vectors = [unit_square_at(DVec2::ZERO), unit_square_at(DVec2::new(3., 1.))]; let arena = core_types::arena::Arena::new(1 << 16).unwrap(); let appearance = fill_appearance(&paint); - let mut builder = RunBuilder::new(&arena, element_write_hashed::(), &[FieldWrite::of::(0), FieldWrite::of::(0)], 2).unwrap(); + let mut builder = RunBuilder::new(&arena, element_write_hashed::(), &[FieldWrite::of::(0)], 2).unwrap(); let lane = builder.push(vectors[0].clone()).unwrap(); - builder.attr::(lane, Some(&paint)); builder.attr::(lane, Some(&appearance)); builder.push(vectors[1].clone()).unwrap(); let item = builder.finish(); @@ -3179,9 +3175,8 @@ mod group_walk_tests { let inner = Graphic::Vector(unit_square_at(DVec2::ZERO)); let arena = core_types::arena::Arena::new(1 << 16).unwrap(); let appearance = fill_appearance(&paint); - let mut builder = RunBuilder::new(&arena, element_write_hashed::(), &[FieldWrite::of::(0), FieldWrite::of::(0)], 1).unwrap(); + let mut builder = RunBuilder::new(&arena, element_write_hashed::(), &[FieldWrite::of::(0)], 1).unwrap(); let lane = builder.push(inner.clone()).unwrap(); - builder.attr::(lane, Some(&paint)); builder.attr::(lane, Some(&appearance)); let item = builder.finish(); let group = Group { row: None, content: item }; @@ -3200,9 +3195,8 @@ mod group_walk_tests { let vectors = [unit_square_at(DVec2::ZERO)]; let arena = core_types::arena::Arena::new(1 << 16).unwrap(); let appearance = fill_appearance(&paint); - let mut builder = RunBuilder::new(&arena, element_write_hashed::(), &[FieldWrite::of::(0), FieldWrite::of::(0)], 1).unwrap(); + let mut builder = RunBuilder::new(&arena, element_write_hashed::(), &[FieldWrite::of::(0)], 1).unwrap(); let lane = builder.push(vectors[0].clone()).unwrap(); - builder.attr::(lane, Some(&paint)); builder.attr::(lane, Some(&appearance)); let item = builder.finish(); let group = Group { row: None, content: item }; @@ -3227,9 +3221,8 @@ mod group_walk_tests { let vectors = [unit_square_at(DVec2::ZERO), unit_square_at(DVec2::new(2., 2.))]; let arena = core_types::arena::Arena::new(1 << 16).unwrap(); let appearance = fill_appearance(&paint); - let mut builder = RunBuilder::new(&arena, element_write_hashed::(), &[FieldWrite::of::(0), FieldWrite::of::(0)], 2).unwrap(); + let mut builder = RunBuilder::new(&arena, element_write_hashed::(), &[FieldWrite::of::(0)], 2).unwrap(); let lane = builder.push(vectors[0].clone()).unwrap(); - builder.attr::(lane, Some(&paint)); builder.attr::(lane, Some(&appearance)); builder.push(vectors[1].clone()).unwrap(); let item = builder.finish(); diff --git a/node-graph/nodes/graphic/src/graphic.rs b/node-graph/nodes/graphic/src/graphic.rs index 6a02ddd7f1..c0cc5d2de1 100644 --- a/node-graph/nodes/graphic/src/graphic.rs +++ b/node-graph/nodes/graphic/src/graphic.rs @@ -181,8 +181,6 @@ fn mirror_lane<'e, T: Clone + Default + Send + Sync + 'static>( ( T, Attr<'e, TransformAttr>, - Attr<'e, graphic_types::markers::Fill>, - Attr<'e, graphic_types::markers::Stroke>, Attr<'e, graphic_types::markers::Appearance>, Attr<'e, core_types::attribute::BlendMode>, Attr<'e, core_types::attribute::Opacity>, @@ -214,20 +212,11 @@ where trace: Vec::new(), }) }; - let park_paint = |paint: Option>>| -> Result>>, Interrupt> { - match paint { - Some(paint) => Ok(Some(arena.alloc_sized_keyed(paint, 0).ok_or_else(exhausted)?.0)), - None => Ok(None), - } - }; - let element = legacy.element(source).cloned().unwrap_or_default(); let mut transform: DAffine2 = legacy.attribute_cloned_or_default(ATTR_TRANSFORM, source); if mirrored { transform = reflected_transform.expect("a mirrored lane exists only under a reflection") * transform; } - let fill = park_paint(legacy.attribute::>>(graphic_types::ATTR_FILL, source).cloned().flatten())?; - let stroke = park_paint(legacy.attribute::>>(graphic_types::ATTR_STROKE, source).cloned().flatten())?; let appearance = match legacy.attribute::(graphic_types::ATTR_APPEARANCE, source).cloned() { Some(appearance) => Some(&*arena.alloc_sized_keyed(appearance, 0).ok_or_else(exhausted)?.0), None => None, @@ -238,8 +227,6 @@ where Ok(( element, Attr(transform), - Attr(fill), - Attr(stroke), Attr(appearance), Attr(legacy.attribute_cloned_or_default(core_types::ATTR_BLEND_MODE, source)), Attr(legacy.attribute_cloned_or(core_types::ATTR_OPACITY, source, 1.)), @@ -272,8 +259,6 @@ fn mirror<'e>( IList<( Graphic<'static>, Attr<'e, TransformAttr>, - Attr<'e, graphic_types::markers::Fill>, - Attr<'e, graphic_types::markers::Stroke>, Attr<'e, graphic_types::markers::Appearance>, Attr<'e, core_types::attribute::BlendMode>, Attr<'e, core_types::attribute::Opacity>, @@ -323,8 +308,6 @@ fn mirror_vector<'e>( IList<( Vector, Attr<'e, TransformAttr>, - Attr<'e, graphic_types::markers::Fill>, - Attr<'e, graphic_types::markers::Stroke>, Attr<'e, graphic_types::markers::Appearance>, Attr<'e, core_types::attribute::BlendMode>, Attr<'e, core_types::attribute::Opacity>, diff --git a/node-graph/nodes/path-bool/src/lib.rs b/node-graph/nodes/path-bool/src/lib.rs index 83322ad3a9..49da3fbcc0 100644 --- a/node-graph/nodes/path-bool/src/lib.rs +++ b/node-graph/nodes/path-bool/src/lib.rs @@ -4,8 +4,8 @@ use core_types::uuid::NodeId; use core_types::{ATTR_BLEND_MODE, ATTR_CLIPPING_MASK, ATTR_EDITOR_LAYER_PATH, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_TRANSFORM, BlendMode, Color, Ctx}; use glam::{DAffine2, DVec2}; use graphic_types::appearance::{Appearance, Coverage}; -use graphic_types::graphic::{GraphicLevel, PaintColumns, PaintReach, bake_paint_transforms, is_paint_present, set_paint_attribute, set_paint_attribute_at}; -use graphic_types::markers::{Appearance as AppearanceMarker, EditorMergedLayers, Fill, Stroke}; +use graphic_types::graphic::{GraphicLevel, PaintColumns, PaintReach, bake_paint_transforms}; +use graphic_types::markers::{Appearance as AppearanceMarker, EditorMergedLayers}; use graphic_types::raster_types::{CPU, GPU, Raster}; use graphic_types::vector_types::GradientStops; use graphic_types::vector_types::gradient::{GradientSpreadMethod, GradientType}; @@ -13,7 +13,7 @@ use graphic_types::vector_types::subpath::{ManipulatorGroup, Subpath}; use graphic_types::vector_types::vector::PointId; use graphic_types::vector_types::vector::algorithms::merge_by_distance::MergeByDistanceExt; use graphic_types::vector_types::{ATTR_GRADIENT_TYPE, ATTR_SPREAD_METHOD}; -use graphic_types::{ATTR_FILL, ATTR_STROKE, Graphic, IntoGraphicList, Vector}; +use graphic_types::{Graphic, IntoGraphicList, Vector}; use linesweeper::topology::Topology; use linesweeper::{BinaryOp, FillRule, binary_op}; use smallvec::SmallVec; @@ -34,8 +34,6 @@ fn boolean_core<'e>( ( Vector, Attr<'e, TransformAttr>, - Attr<'e, Fill>, - Attr<'e, Stroke>, Attr<'e, AppearanceMarker>, Attr<'e, BlendModeAttr>, Attr<'e, Opacity>, @@ -69,17 +67,8 @@ fn boolean_core<'e>( trace: Vec::new(), }) }; - let park_paint = |paint: Option>>| -> Result>, core_types::gpoll::Interrupt> { - match paint { - Some(list) => Ok(Some(arena.alloc_sized_keyed(list, 0).ok_or_else(exhausted)?.0)), - None => Ok(None), - } - }; - let element = result_vector_list.element(0).cloned().unwrap_or_default(); use core_types::lane::LaneSource; - let fill = park_paint(result_vector_list.attr::(0).filter(|paint| is_paint_present(paint)).cloned())?; - let stroke = park_paint(result_vector_list.attr::(0).filter(|paint| is_paint_present(paint)).cloned())?; let appearance = match result_vector_list.attr::(0).cloned() { Some(appearance) => Some(&*arena.alloc_sized_keyed(appearance, 0).ok_or_else(exhausted)?.0), None => None, @@ -93,8 +82,6 @@ fn boolean_core<'e>( Ok(( element, Attr(result_vector_list.attribute_cloned_or_default(ATTR_TRANSFORM, 0)), - Attr(fill), - Attr(stroke), Attr(appearance), Attr(result_vector_list.attribute_cloned_or_default(ATTR_BLEND_MODE, 0)), Attr(result_vector_list.attribute_cloned_or(ATTR_OPACITY, 0, 1.)), @@ -122,8 +109,6 @@ fn boolean_operation<'e>( ( Vector, Attr<'e, TransformAttr>, - Attr<'e, Fill>, - Attr<'e, Stroke>, Attr<'e, AppearanceMarker>, Attr<'e, BlendModeAttr>, Attr<'e, Opacity>, @@ -150,8 +135,6 @@ fn boolean_operation_vector<'e>( ( Vector, Attr<'e, TransformAttr>, - Attr<'e, Fill>, - Attr<'e, Stroke>, Attr<'e, AppearanceMarker>, Attr<'e, BlendModeAttr>, Attr<'e, Opacity>, @@ -309,14 +292,13 @@ fn raster_stand_in_rows(image: &S, parent_trans .with_attribute(ATTR_OPACITY_FILL, fill) .with_attribute(ATTR_CLIPPING_MASK, clip) .with_attribute(ATTR_EDITOR_LAYER_PATH, layer); - set_paint_attribute(item.attributes_mut(), ATTR_FILL, List::new_from_element(Color::BLACK)); item.set_attribute(graphic_types::ATTR_APPEARANCE, fill_appearance(List::new_from_element(Graphic::Color(Color::BLACK)))); item }) .collect() } -/// The single-fill appearance a built row paints with, beside its legacy fill marker. +/// The single-fill appearance a built row paints with. fn fill_appearance(paint: List>) -> Appearance { Appearance::new_single(Coverage::new_fill(), Graphic::Graphic(paint)) } @@ -324,7 +306,6 @@ fn fill_appearance(paint: List>) -> Appearance { /// A color row: an empty vector carrying the color as its fill paint over the /// lane's attributes. fn color_paint_row(color: Color, mut attributes: core_types::list::ItemAttributeValues) -> Item { - set_paint_attribute(&mut attributes, ATTR_FILL, List::new_from_element(color)); attributes.insert(graphic_types::ATTR_APPEARANCE, fill_appearance(List::new_from_element(Graphic::Color(color)))); let mut element = Vector::default(); @@ -346,7 +327,6 @@ fn gradient_paint_row(stops: GradientStops, mut attributes: core_types::list::It if let Some(spread_method) = attributes.remove::(ATTR_SPREAD_METHOD) { gradient_paint.set_attribute(ATTR_SPREAD_METHOD, 0, spread_method); } - attributes.insert(ATTR_FILL, Some(gradient_paint.clone())); attributes.insert(graphic_types::ATTR_APPEARANCE, fill_appearance(gradient_paint)); let mut element = Vector::default(); @@ -378,13 +358,6 @@ fn push_rows(out: &mut List, rows: Vec>) { fn push_leaf_vector_row(out: &mut List, level: GraphicLevel<'_>, index: usize, vector: &Vector, ancestors: DAffine2, reach: PaintReach<'_>) { let out_index = out.len(); out.push(Item::from_parts(vector.clone(), graphic_types::graphic::lane_attributes(level, index))); - if reach.applies() { - for (key, slot) in [(ATTR_FILL, reach.paint.fill), (ATTR_STROKE, reach.paint.stroke)] { - if let Some(paint) = slot { - set_paint_attribute_at(out, out_index, key, paint.clone()); - } - } - } stamp_inherited_appearance(out, out_index, reach.appearance); let current: DAffine2 = out.attribute_cloned_or_default(ATTR_TRANSFORM, out_index); out.set_attribute(ATTR_TRANSFORM, out_index, ancestors * current); @@ -395,13 +368,6 @@ fn push_vector_rows(out: &mut List, rows: &List, composed: DAffi let Some(item) = rows.clone_item(row) else { continue }; let index = out.len(); out.push(item); - if reach.applies() { - for (key, slot) in [(ATTR_FILL, reach.paint.fill), (ATTR_STROKE, reach.paint.stroke)] { - if let Some(paint) = slot { - set_paint_attribute_at(out, index, key, paint.clone()); - } - } - } stamp_inherited_appearance(out, index, reach.appearance); let current: DAffine2 = out.attribute_cloned_or_default(ATTR_TRANSFORM, index); out.set_attribute(ATTR_TRANSFORM, index, composed * current); @@ -441,7 +407,7 @@ fn flatten_vector_run_into<'a>(out: &mut List, level: GraphicLevel<'a>, let composed = transform * level.attr::(index); match element { Graphic::Vector(vector) => push_leaf_vector_row(out, level, index, vector, transform, reach), - Graphic::Graphic(children) => push_union(out, flatten_vector_run(GraphicLevel::Legacy(children), composed, reach.nested())), + Graphic::Graphic(children) => push_union(out, flatten_vector_run(GraphicLevel::Legacy(children), composed, reach)), Graphic::Group(group) => flatten_group(out, group, composed, reach), Graphic::RasterCPU(raster) => push_rows(out, raster_stand_in_rows(&LeafLane::new(&level, index, raster), transform)), Graphic::RasterGPU(raster) => push_rows(out, raster_stand_in_rows(&LeafLane::new(&level, index, raster), transform)), @@ -463,7 +429,7 @@ fn flatten_group(out: &mut List, group: &core_types::record::Group, comp if let Some(rows) = graphic_types::graphic::run_to_list::(item) { push_vector_rows(out, &rows, composed, reach); } else if core_types::record::RunView::::new(item).is_some() { - push_union(out, flatten_vector_run(GraphicLevel::Run(item), composed, reach.into_group_graphics())); + push_union(out, flatten_vector_run(GraphicLevel::Run(item), composed, reach)); } else if let Some(image) = graphic_types::graphic::run_to_list::>(item) { push_rows(out, raster_stand_in_rows(&image, composed)); } else if let Some(image) = graphic_types::graphic::run_to_list::>(item) { @@ -598,22 +564,28 @@ mod tests { top.push(Item::new_from_element(Graphic::Color(Color::BLACK))); top.push(Item::new_from_element(Graphic::Group(Group { row: None, content: inner_item }))); top.set_attribute(ATTR_TRANSFORM, 0, DAffine2::from_translation(DVec2::new(5., 5.))); - set_paint_attribute_at(&mut top, 0, ATTR_FILL, black_paint()); + top.set_attribute(graphic_types::ATTR_APPEARANCE, 0, fill_appearance(black_paint())); top.set_attribute(ATTR_OPACITY, 1, 0.5); top.set_attribute(ATTR_TRANSFORM, 2, DAffine2::from_scale(DVec2::splat(3.))); let rows = flatten_vector_run(GraphicLevel::Legacy(&top), DAffine2::IDENTITY, PaintReach::NONE); assert_eq!(rows.len(), 3); + let fill_of = |index: usize| { + rows.attribute::(graphic_types::ATTR_APPEARANCE, index) + .and_then(|appearance| appearance.first_paint_of(graphic_types::appearance::Cover::Fill)) + .and_then(graphic_types::graphic::paint_cell_rows) + }; + // Lane 0: the leaf row keeps its lane attributes, with the lane fill // present and the ancestor composition the identity. assert_eq!(rows.attribute_cloned_or_default::(ATTR_TRANSFORM, 0), DAffine2::from_translation(DVec2::new(5., 5.))); - assert!(graphic_types::graphic::paint_graphics::(&rows, 0).is_some()); + assert!(fill_of(0).is_some()); // Lane 1: the color stand-in carries the lane opacity and the color as // its fill. assert_eq!(rows.attribute_cloned_or::(ATTR_OPACITY, 1, 1.), 0.5); - let fill = graphic_types::graphic::paint_graphics::(&rows, 1).expect("the color row carries its fill"); + let fill = fill_of(1).expect("the color row carries its fill"); assert!(matches!(fill.element(0), Some(Graphic::Color(color)) if *color == Color::BLACK)); // Lane 2: the group's vector run serves its row under the lane diff --git a/node-graph/nodes/vector/src/vector_nodes.rs b/node-graph/nodes/vector/src/vector_nodes.rs index 17a03d7ab6..8919a94542 100644 --- a/node-graph/nodes/vector/src/vector_nodes.rs +++ b/node-graph/nodes/vector/src/vector_nodes.rs @@ -18,10 +18,10 @@ use core_types::uuid::NodeId; use core_types::{ATTR_BLEND_MODE, ATTR_CLIPPING_MASK, ATTR_EDITOR_LAYER_PATH, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_TRANSFORM, CacheHash, Color, Ctx, DeriveCtx, ExtractIndex, InjectIndex}; use glam::{DAffine2, DMat2, DVec2}; use graphic_types::appearance::{Appearance, Cover, CoverPlacement, Coverage}; -use graphic_types::graphic::{bake_paint_transforms, has_paint, is_paint_present, set_paint_attribute_at}; -use graphic_types::markers::{Appearance as AppearanceMarker, EditorMergedLayers, Fill, Stroke as StrokeAttr}; +use graphic_types::graphic::{bake_paint_transforms, is_paint_present}; +use graphic_types::markers::{Appearance as AppearanceMarker, EditorMergedLayers}; use graphic_types::raster_types::{CPU, GPU, Raster}; -use graphic_types::{ATTR_EDITOR_MERGED_LAYERS, ATTR_FILL, ATTR_STROKE, Graphic, IntoGraphicList}; +use graphic_types::{ATTR_EDITOR_MERGED_LAYERS, Graphic, IntoGraphicList}; use graphic_types::{Artboard, Vector}; use kurbo::simplify::{SimplifyOptions, simplify_bezpath}; use kurbo::{Affine, BezPath, DEFAULT_ACCURACY, Line, ParamCurve, ParamCurveArclen, PathEl, PathSeg, Shape}; @@ -101,33 +101,19 @@ fn assign_colors<'e>( /// The number of elements to span across the gradient before repeating. A 0 value will span the entire gradient once. #[widget(ParsedWidgetOverride::Custom = "assign_colors_repeat_every")] repeat_every: u32, -) -> Result< - IList<( - Vector, - Attr<'e, TransformAttr>, - Attr<'e, Fill>, - Attr<'e, StrokeAttr>, - Attr<'e, AppearanceMarker>, - Attr<'e, EditorLayerPath>, - )>, - Interrupt, -> { +) -> Result, Attr<'e, AppearanceMarker>, Attr<'e, EditorLayerPath>)>, Interrupt> { let lane = ctx.index() as usize; if lane >= content.len() { return Err(GraphError::past_end().into()); } let element = content.element_ref(lane).clone(); - let park_existing = |paint: Option<&List>>| -> Result>, Interrupt> { paint.map(|paint| park_paint(ctx.arena(), paint.clone())).transpose() }; - let existing_fill = park_existing(content.lane(lane).attr::())?; - let existing_stroke = park_existing(content.lane(lane).attr::())?; let existing_appearance = content.lane(lane).attr::().cloned(); let park_appearance_attr = |appearance: Option| -> Result, Interrupt> { appearance.map(|appearance| park_appearance(ctx.arena(), appearance)).transpose() }; - let carried = carried_lane_attrs(ctx.arena(), *content.lane(lane))?; - let (transform, layer_path) = carried; + let (transform, layer_path) = carried_lane_attrs(ctx.arena(), *content.lane(lane))?; if gradient.is_empty() { let parked_appearance = park_appearance_attr(existing_appearance)?; - return Ok((element, transform, Attr(existing_fill), Attr(existing_stroke), Attr(parked_appearance), layer_path)); + return Ok((element, transform, Attr(parked_appearance), layer_path)); } let gradient_element = gradient.element_ref(0); let reversed; @@ -141,20 +127,10 @@ fn assign_colors<'e>( let color = assign_color_at(gradient_element, lane, content.len(), randomize, seed, repeat_every); let paint = List::new_from_element(color).into_graphic_list(); - let parked = park_paint(ctx.arena(), paint)?; - let fill_attr = match fill { - true => Some(parked), - false => existing_fill, - }; - let stroke_attr = match stroke && element.stroke.is_some() { - true => Some(parked), - false => existing_stroke, - }; - - // The same recolor lands on the appearance's coverage paints + // The recolor lands on the appearance's coverage paints let mut appearance = existing_appearance.unwrap_or_default(); - let paint_cell = Graphic::Graphic(parked.clone()); + let paint_cell = Graphic::Graphic(paint); if fill && !appearance.set_paint_of(Cover::Fill, paint_cell.clone()) { appearance.replace_or_insert(Coverage::new_fill(), paint_cell.clone(), CoverPlacement::Above); } @@ -163,7 +139,7 @@ fn assign_colors<'e>( } let parked_appearance = park_appearance_attr(Some(appearance))?; - Ok((element, transform, Attr(fill_attr), Attr(stroke_attr), Attr(parked_appearance), layer_path)) + Ok((element, transform, Attr(parked_appearance), layer_path)) } #[allow(clippy::too_many_arguments)] @@ -257,16 +233,10 @@ fn assign_colors_graphic<'e>( let row_stroke = rows.element(row).and_then(|vector| vector.stroke.clone()); let color = assign_color_at(gradient_element, position + row, length, randomize, seed, repeat_every); let paint = List::new_from_element(color).into_graphic_list(); - if fill { - set_paint_attribute_at(&mut rows, row, ATTR_FILL, paint.clone()); - } - if stroke && row_stroke.is_some() { - set_paint_attribute_at(&mut rows, row, ATTR_STROKE, paint.clone()); - } - // The same recolor lands on the row's appearance coverage paints + // The recolor lands on the row's appearance coverage paints let mut appearance = rows.attribute_cloned_or_default::(graphic_types::ATTR_APPEARANCE, row); - let paint_cell = Graphic::Graphic(paint.clone()); + let paint_cell = Graphic::Graphic(paint); if fill && !appearance.set_paint_of(Cover::Fill, paint_cell.clone()) { appearance.replace_or_insert(Coverage::new_fill(), paint_cell.clone(), CoverPlacement::Above); } @@ -313,17 +283,8 @@ fn assign_colors_graphic_extent( pub use _assign_colors_graphic_mod::assign_colors_graphic_entries; -/// Keyed, so a group-free paint's promote moves this header rather than +/// Keyed, so a group-free appearance's promote moves this header rather than /// cloning the content it owns. -fn park_paint<'e>(arena: &'e core_types::arena::Arena, paint: List>) -> Result<&'e List>, Interrupt> { - let (parked, _) = arena.alloc_sized_keyed(paint, 0).ok_or(GraphError { - kind: core_types::gpoll::ErrorKind::ArenaExhausted, - trace: Vec::new(), - })?; - Ok(parked) -} - -/// Keyed, as [`park_paint`] is, so a group-free appearance's promote moves this header. fn park_appearance<'e>(arena: &'e core_types::arena::Arena, appearance: Appearance) -> Result<&'e Appearance, Interrupt> { let (parked, _) = arena.alloc_sized_keyed(appearance, 0).ok_or(GraphError { kind: core_types::gpoll::ErrorKind::ArenaExhausted, @@ -397,13 +358,12 @@ fn fill<'e>( _spread_method: GradientSpreadMethod, _has_transform: HasTransform, _transform: DAffine2, -) -> Result<(Vector, Attr<'e, Fill>, Attr<'e, AppearanceMarker>), Interrupt> { +) -> Result<(Vector, Attr<'e, AppearanceMarker>), Interrupt> { let mut paint = paint_table(fill); default_gradient_paint(&mut paint, element.bounding_box(), _gradient_type, _spread_method, _has_transform.0.then_some(_transform)); let appearance = stamped_appearance(*content_appearance, Coverage::new_fill(), &paint, CoverPlacement::Above); - let parked = park_paint(ctx.arena(), paint)?; let parked_appearance = park_appearance(ctx.arena(), appearance)?; - Ok((element, Attr(Some(parked)), Attr(Some(parked_appearance)))) + Ok((element, Attr(Some(parked_appearance)))) } /// The fill over graphic lanes: the marker parks on the lane and the render @@ -420,7 +380,7 @@ fn fill_graphic_leveled<'e>( _spread_method: GradientSpreadMethod, _has_transform: HasTransform, _transform: DAffine2, -) -> Result<(Graphic<'static>, Attr<'e, Fill>, Attr<'e, AppearanceMarker>), Interrupt> { +) -> Result<(Graphic<'static>, Attr<'e, AppearanceMarker>), Interrupt> { let bounds = match BoundingBox::bounding_box(&element, DAffine2::IDENTITY, false) { RenderBoundingBox::Rectangle(bounds) => Some(bounds), _ => None, @@ -428,9 +388,8 @@ fn fill_graphic_leveled<'e>( let mut paint = paint_table(fill); default_gradient_paint(&mut paint, bounds, _gradient_type, _spread_method, _has_transform.0.then_some(_transform)); let appearance = stamped_appearance(*content_appearance, Coverage::new_fill(), &paint, CoverPlacement::Above); - let parked = park_paint(ctx.arena(), paint)?; let parked_appearance = park_appearance(ctx.arena(), appearance)?; - Ok((element, Attr(Some(parked)), Attr(Some(parked_appearance)))) + Ok((element, Attr(Some(parked_appearance)))) } /// Applies a stroke style to the vector content, giving an appearance to the area within the outline of the geometry. @@ -460,7 +419,7 @@ fn stroke<'e>( /// The phase offset distance from the starting point of the dash pattern. #[unit(" px")] dash_offset: f64, -) -> Result<(Vector, Attr, Attr<'e, StrokeAttr>, Attr<'e, AppearanceMarker>), Interrupt> { +) -> Result<(Vector, Attr, Attr<'e, AppearanceMarker>), Interrupt> { let dash_lengths: Vec = (0..dash_lengths.len()).map(|index| dash_lengths.get(index).max(0.)).collect(); let mut stroke = Stroke { weight, @@ -487,9 +446,8 @@ fn stroke<'e>( // The paint order is the coverage row order: appending above follows the painter's algorithm, and a // below stroke is expressed by the chain running the stroke node before the fill let appearance = stamped_appearance(*content_appearance, Coverage::new_stroke(&coverage_stroke), &paint, CoverPlacement::Above); - let parked = park_paint(ctx.arena(), paint)?; let parked_appearance = park_appearance(ctx.arena(), appearance)?; - Ok((element, Attr(*content_transform), Attr(Some(parked)), Attr(Some(parked_appearance)))) + Ok((element, Attr(*content_transform), Attr(Some(parked_appearance)))) } /// The vector items of a graphic lane's interior, one wrap level deep, the @@ -526,7 +484,7 @@ fn stroke_graphic_leveled<'e>( #[default(4.)] miter_limit: f64, dash_lengths: IList, #[unit(" px")] dash_offset: f64, -) -> Result<(Graphic<'static>, Attr, Attr<'e, StrokeAttr>, Attr<'e, AppearanceMarker>), Interrupt> { +) -> Result<(Graphic<'static>, Attr, Attr<'e, AppearanceMarker>), Interrupt> { let dash_lengths: Vec = (0..dash_lengths.len()).map(|index| dash_lengths.get(index).max(0.)).collect(); let stroke = Stroke { weight, @@ -554,9 +512,8 @@ fn stroke_graphic_leveled<'e>( // The paint order is the coverage row order: appending above follows the painter's algorithm, and a // below stroke is expressed by the chain running the stroke node before the fill let appearance = stamped_appearance(*content_appearance, Coverage::new_stroke(&coverage_stroke), &paint, CoverPlacement::Above); - let parked = park_paint(ctx.arena(), paint)?; let parked_appearance = park_appearance(ctx.arena(), appearance)?; - Ok((element, Attr(*content_transform), Attr(Some(parked)), Attr(Some(parked_appearance)))) + Ok((element, Attr(*content_transform), Attr(Some(parked_appearance)))) } pub use _fill_graphic_leveled_mod::fill_graphic_leveled_entries; @@ -1438,8 +1395,14 @@ fn offset_path(_: impl Ctx, (vector, lane_transform): (Vector, Attr) -> List { // TODO: Make this node support stroke align, which it currently ignores - // A fill exists when the canonical attribute carries paint - let has_fills: Vec = (0..flattened.len()).map(|index| has_paint::(&flattened, index)).collect(); + // A fill exists when the row's appearance carries a painted fill coverage + let has_fills: Vec = (0..flattened.len()) + .map(|index| { + flattened + .attribute::(graphic_types::ATTR_APPEARANCE, index) + .is_some_and(|appearance| appearance.has_painted_cover(Cover::Fill)) + }) + .collect(); let output: List = flattened .into_iter() @@ -1503,7 +1466,6 @@ fn solidify_rows(flattened: List) -> List { vector.stroke = None; let mut fill_attributes = attributes.clone(); // No stroke remains on the fill row - fill_attributes.remove::>>(ATTR_STROKE); if let Some(appearance) = fill_attributes.get_mut::(graphic_types::ATTR_APPEARANCE) { appearance.retain_cover(Cover::Fill); } @@ -1512,8 +1474,6 @@ fn solidify_rows(flattened: List) -> List { let mut stroke_attributes = attributes; // Drop the original fill and use the stroke paint to fill the outlined stroke - stroke_attributes.remove::>>(ATTR_FILL); - stroke_attributes.rename(ATTR_STROKE, ATTR_FILL); if let Some(appearance) = stroke_attributes.get_mut::(graphic_types::ATTR_APPEARANCE) { // The outlined stroke is filled with the stroke coverage's paint let stroke_paint = appearance.first_paint_of(Cover::Stroke).cloned(); @@ -1549,8 +1509,6 @@ fn solidify_native_lane<'e>( ( Vector, Attr<'e, TransformAttr>, - Attr<'e, Fill>, - Attr<'e, StrokeAttr>, Attr<'e, AppearanceMarker>, Attr<'e, BlendModeAttr>, Attr<'e, Opacity>, @@ -1610,8 +1568,6 @@ fn emit_legacy_lane<'e>( ( Vector, Attr<'e, TransformAttr>, - Attr<'e, Fill>, - Attr<'e, StrokeAttr>, Attr<'e, AppearanceMarker>, Attr<'e, BlendModeAttr>, Attr<'e, Opacity>, @@ -1633,16 +1589,6 @@ fn emit_legacy_lane<'e>( }; let element = output.element(lane).cloned().unwrap_or_default(); - let fill = output - .attribute::>>(ATTR_FILL, lane) - .and_then(|paint| paint.as_ref()) - .map(|paint| park_paint(arena, paint.clone())) - .transpose()?; - let stroke = output - .attribute::>>(ATTR_STROKE, lane) - .and_then(|paint| paint.as_ref()) - .map(|paint| park_paint(arena, paint.clone())) - .transpose()?; let appearance = output .attribute::(graphic_types::ATTR_APPEARANCE, lane) .cloned() @@ -1659,8 +1605,6 @@ fn emit_legacy_lane<'e>( Ok(( element, Attr(output.attribute_cloned_or_default(ATTR_TRANSFORM, lane)), - Attr(fill), - Attr(stroke), Attr(appearance), Attr(output.attribute_cloned_or_default(ATTR_BLEND_MODE, lane)), Attr(output.attribute_cloned_or(ATTR_OPACITY, lane, 1.)), @@ -1707,8 +1651,6 @@ fn solidify_stroke<'e>( IList<( Vector, Attr<'e, TransformAttr>, - Attr<'e, Fill>, - Attr<'e, StrokeAttr>, Attr<'e, AppearanceMarker>, Attr<'e, BlendModeAttr>, Attr<'e, Opacity>, @@ -1748,8 +1690,6 @@ fn solidify_stroke_vector<'e>( IList<( Vector, Attr<'e, TransformAttr>, - Attr<'e, Fill>, - Attr<'e, StrokeAttr>, Attr<'e, AppearanceMarker>, Attr<'e, BlendModeAttr>, Attr<'e, Opacity>, @@ -1821,8 +1761,6 @@ fn separate_subpaths<'e>( IList<( Vector, Attr<'e, TransformAttr>, - Attr<'e, Fill>, - Attr<'e, StrokeAttr>, Attr<'e, AppearanceMarker>, Attr<'e, BlendModeAttr>, Attr<'e, Opacity>, @@ -1878,8 +1816,6 @@ fn map_points<'e>( IList<( Vector, Attr<'e, TransformAttr>, - Attr<'e, Fill>, - Attr<'e, StrokeAttr>, Attr<'e, AppearanceMarker>, Attr<'e, BlendModeAttr>, Attr<'e, Opacity>, @@ -1916,18 +1852,7 @@ fn flatten_path_core<'e>( arena: &'e core_types::arena::Arena, flattened: List, snapshot: List>, -) -> Result< - ( - Vector, - Attr<'e, TransformAttr>, - Attr<'e, Fill>, - Attr<'e, StrokeAttr>, - Attr<'e, AppearanceMarker>, - Attr<'e, EditorLayerPath>, - Attr<'e, EditorMergedLayers>, - ), - Interrupt, -> { +) -> Result<(Vector, Attr<'e, TransformAttr>, Attr<'e, AppearanceMarker>, Attr<'e, EditorLayerPath>, Attr<'e, EditorMergedLayers>), Interrupt> { let mut output = Vector::default(); let mut primary_source = None; @@ -1951,28 +1876,20 @@ fn flatten_path_core<'e>( primary_source = Some((index, source_transform)); } - let mut fill = None; - let mut stroke = None; + let mut fill_cell = None; + let mut stroke_cell = None; let mut layer_path = Vec::new(); if let Some((primary, source_transform)) = primary_source { let source_attributes = flattened.clone_item_attributes(primary); let mut attributes = ItemAttributeValues::new(); - attributes.insert_cloned_from(&source_attributes, ATTR_FILL); - attributes.insert_cloned_from(&source_attributes, ATTR_STROKE); + attributes.insert_cloned_from(&source_attributes, graphic_types::ATTR_APPEARANCE); bake_paint_transforms(&mut attributes, source_transform); - let carrier = List::new_from_item(Item::from_parts(Vector::default(), attributes)); - fill = carrier - .attribute::>>(ATTR_FILL, 0) - .and_then(|paint| paint.as_ref()) - .map(|paint| park_paint(arena, paint.clone())) - .transpose()?; - stroke = carrier - .attribute::>>(ATTR_STROKE, 0) - .and_then(|paint| paint.as_ref()) - .map(|paint| park_paint(arena, paint.clone())) - .transpose()?; + if let Some(appearance) = attributes.get::(graphic_types::ATTR_APPEARANCE) { + fill_cell = appearance.first_paint_of(Cover::Fill).filter(|cell| !cell.is_empty()).cloned(); + stroke_cell = appearance.first_paint_of(Cover::Stroke).filter(|cell| !cell.is_empty()).cloned(); + } // Adopt the last input item's layer so the editor can also bucket clicks under a contributing child layer layer_path = flattened.attribute_cloned_or_default::>(ATTR_EDITOR_LAYER_PATH, primary); @@ -1988,15 +1905,15 @@ fn flatten_path_core<'e>( // editor click-target preservation, as the boolean operation does. let merged_layers = arena.alloc_sized_keyed(snapshot, 0).ok_or_else(exhausted)?.0; - // The carried paints land on the appearance too, the stroke coverage recording the carried stroke's parameters + // The carried paints land on the appearance, the stroke coverage recording the carried stroke's parameters let appearance = { let mut appearance = Appearance::default(); - if let Some(fill_paint) = fill { - appearance.replace_or_insert(Coverage::new_fill(), Graphic::Graphic(fill_paint.clone()), CoverPlacement::Above); + if let Some(cell) = fill_cell { + appearance.replace_or_insert(Coverage::new_fill(), cell, CoverPlacement::Above); } - if let Some(stroke_paint) = stroke { + if let Some(cell) = stroke_cell { let coverage = Coverage::new_stroke(&output.stroke.clone().unwrap_or_default()); - appearance.replace_or_insert(coverage, Graphic::Graphic(stroke_paint.clone()), CoverPlacement::Above); + appearance.replace_or_insert(coverage, cell, CoverPlacement::Above); } match appearance.declared().is_some() { true => Some(park_appearance(arena, appearance)?), @@ -2004,15 +1921,7 @@ fn flatten_path_core<'e>( } }; - Ok(( - output, - Attr(DAffine2::IDENTITY), - Attr(fill), - Attr(stroke), - Attr(appearance), - Attr(layer_path.as_slice()), - Attr(Some(merged_layers)), - )) + Ok((output, Attr(DAffine2::IDENTITY), Attr(appearance), Attr(layer_path.as_slice()), Attr(Some(merged_layers)))) } // TODO: Rename to "Combine Paths" and make this happen per-element instead of flattening every element into a single path. The migration for this should then become a Flatten Vector -> Combine Paths pair of nodes. @@ -2020,18 +1929,7 @@ fn flatten_path_core<'e>( pub fn flatten_path<'e>( ctx: impl Ctx + ExtractArena<'e> + ExtractIndex + InjectIndex + Copy, content: IList>, -) -> Result< - ( - Vector, - Attr<'e, TransformAttr>, - Attr<'e, Fill>, - Attr<'e, StrokeAttr>, - Attr<'e, AppearanceMarker>, - Attr<'e, EditorLayerPath>, - Attr<'e, EditorMergedLayers>, - ), - Interrupt, -> { +) -> Result<(Vector, Attr<'e, TransformAttr>, Attr<'e, AppearanceMarker>, Attr<'e, EditorLayerPath>, Attr<'e, EditorMergedLayers>), Interrupt> { let item = content.as_group_item(); let flattened = graphic_types::graphic::flatten_vector_rows(graphic_types::graphic::GraphicLevel::Run(&item)); let snapshot = graphic_types::graphic::run_to_list::(&item).expect("the run holds the row's element type"); @@ -2044,18 +1942,7 @@ pub fn flatten_path<'e>( pub fn flatten_path_vector<'e>( ctx: impl Ctx + ExtractArena<'e> + ExtractIndex + InjectIndex + Copy, content: IList, -) -> Result< - ( - Vector, - Attr<'e, TransformAttr>, - Attr<'e, Fill>, - Attr<'e, StrokeAttr>, - Attr<'e, AppearanceMarker>, - Attr<'e, EditorLayerPath>, - Attr<'e, EditorMergedLayers>, - ), - Interrupt, -> { +) -> Result<(Vector, Attr<'e, TransformAttr>, Attr<'e, AppearanceMarker>, Attr<'e, EditorLayerPath>, Attr<'e, EditorMergedLayers>), Interrupt> { let wrapper = wrap_vector_level(content); let flattened = graphic_types::graphic::flatten_vector_rows(graphic_types::graphic::GraphicLevel::Legacy(&wrapper)); let snapshot = legacy_graphic_list_of(content); @@ -2374,8 +2261,6 @@ fn cut_path<'e>( IList<( Vector, Attr<'e, TransformAttr>, - Attr<'e, Fill>, - Attr<'e, StrokeAttr>, Attr<'e, AppearanceMarker>, Attr<'e, BlendModeAttr>, Attr<'e, Opacity>, @@ -2755,7 +2640,6 @@ fn offset_points( /// /// *Progression* morphs through all objects. Interpolation is linear unless *Path* geometry is provided to control the trajectory between key objects. The **Origins to Polyline** node may be used to create a path with anchor points corresponding to each object. Other nodes can modify its path segments. fn morph_core(flattened: List, snapshot: List>, progression: f64, reverse: bool, distribution: InterpolationDistribution, path: List) -> List { - use core_types::lane::LaneSource; /// Promotes a segment's handle pair to cubic-equivalent Bézier control points. /// For linear segments (both None), handles are placed at their respective anchors (zero-length) /// so that interpolation against another zero-length cubic doesn't introduce unwanted curvature. @@ -3193,16 +3077,14 @@ fn morph_core(flattened: List, snapshot: List>, progres }; let mut vector = Vector { stroke, ..Default::default() }; - let fill_paint = { - let source = content.attr::(source_index).filter(|paint| is_paint_present(paint)); - let target = content.attr::(target_index).filter(|paint| is_paint_present(paint)); - lerp_graphic(source, target, time) - }; - let stroke_paint = { - let source = content.attr::(source_index).filter(|paint| is_paint_present(paint)); - let target = content.attr::(target_index).filter(|paint| is_paint_present(paint)); - lerp_graphic(source, target, time) + let coverage_paint = |index: usize, cover: Cover| { + content + .attribute::(graphic_types::ATTR_APPEARANCE, index) + .and_then(|appearance| appearance.first_paint_of(cover)) + .and_then(graphic_types::graphic::paint_cell_rows) }; + let fill_paint = lerp_graphic(coverage_paint(source_index, Cover::Fill), coverage_paint(target_index, Cover::Fill), time); + let stroke_paint = lerp_graphic(coverage_paint(source_index, Cover::Stroke), coverage_paint(target_index, Cover::Stroke), time); // Work directly with manipulator groups, bypassing the BezPath intermediate representation. // This avoids the full Vector → BezPath → interpolate → BezPath → Vector roundtrip each frame. @@ -3361,14 +3243,12 @@ fn morph_core(flattened: List, snapshot: List>, progres .with_attribute(ATTR_EDITOR_LAYER_PATH, layer_path) .with_attribute(ATTR_EDITOR_MERGED_LAYERS, Some(graphic_list_content)); - // The lerped paints land on the appearance too, the stroke coverage recording the lerped stroke's parameters + // The lerped paints land on the appearance, the stroke coverage recording the lerped stroke's parameters let mut appearance = Appearance::default(); if let Some(fill) = fill_paint { - item.set_attribute(ATTR_FILL, Some(fill.clone())); appearance.replace_or_insert(Coverage::new_fill(), Graphic::Graphic(fill), CoverPlacement::Above); } if let Some(stroke) = stroke_paint { - item.set_attribute(ATTR_STROKE, Some(stroke.clone())); let coverage = Coverage::new_stroke(&item.element().stroke.clone().unwrap_or_default()); appearance.replace_or_insert(coverage, Graphic::Graphic(stroke), CoverPlacement::Above); } @@ -3394,8 +3274,6 @@ fn morph_lane<'e>( ( Vector, Attr<'e, TransformAttr>, - Attr<'e, Fill>, - Attr<'e, StrokeAttr>, Attr<'e, AppearanceMarker>, Attr<'e, BlendModeAttr>, Attr<'e, Opacity>, @@ -3435,8 +3313,6 @@ fn morph<'e>( ( Vector, Attr<'e, TransformAttr>, - Attr<'e, Fill>, - Attr<'e, StrokeAttr>, Attr<'e, AppearanceMarker>, Attr<'e, BlendModeAttr>, Attr<'e, Opacity>, @@ -3468,8 +3344,6 @@ fn morph_vector<'e>( ( Vector, Attr<'e, TransformAttr>, - Attr<'e, Fill>, - Attr<'e, StrokeAttr>, Attr<'e, AppearanceMarker>, Attr<'e, BlendModeAttr>, Attr<'e, Opacity>, @@ -3916,7 +3790,6 @@ fn centroid(_: impl Ctx, vector: IList, centroid_type: CentroidType) -> mod test { use super::*; use core_types::transform::Footprint; - use graphic_types::graphic::paint_graphics; use kurbo::{CubicBez, Ellipse, Point, Rect}; use vector_types::vector::algorithms::bezpath_algorithms::{TValue, trim_pathseg}; use vector_types::vector::misc::pathseg_abs_diff_eq; @@ -4072,12 +3945,13 @@ mod test { v }; + let fill_appearance = |color: Color| Appearance::new_single(Coverage::new_fill(), Graphic::Graphic(List::new_from_element(color).into_graphic_list())); let item_a = Item::new_from_element(rect()) .with_attribute(ATTR_TRANSFORM, DAffine2::IDENTITY) - .with_attribute(ATTR_FILL, Some(List::new_from_element(Color::RED).into_graphic_list())); + .with_attribute(graphic_types::ATTR_APPEARANCE, fill_appearance(Color::RED)); let item_b = Item::new_from_element(rect()) .with_attribute(ATTR_TRANSFORM, DAffine2::from_translation((-100., -100.).into())) - .with_attribute(ATTR_FILL, Some(List::new_from_element(Color::BLUE).into_graphic_list())); + .with_attribute(graphic_types::ATTR_APPEARANCE, fill_appearance(Color::BLUE)); let mut content = List::new_from_item(item_a); content.push(item_b); @@ -4085,7 +3959,13 @@ mod test { let snapshot = content.into_graphic_list(); let morphed = super::morph_core(snapshot.clone().into_flattened_list(), snapshot, 0.5, false, InterpolationDistribution::default(), List::default()); - let fill = paint_graphics::(&morphed, 0).expect("Morph should keep the fill paint at the midpoint"); + let appearance = morphed + .attribute::(graphic_types::ATTR_APPEARANCE, 0) + .expect("Morph should keep the appearance at the midpoint"); + let fill = appearance + .first_paint_of(Cover::Fill) + .and_then(graphic_types::graphic::paint_cell_rows) + .expect("Morph should keep the fill paint at the midpoint"); // Interpolated color between red and blue should have >0 value on both R and B let Some(Graphic::Color(color)) = fill.element(0) else {