diff --git a/node-graph/libraries/graphic-types/src/graphic/mod.rs b/node-graph/libraries/graphic-types/src/graphic/mod.rs index 2de8cd5856..b6b53aa36f 100644 --- a/node-graph/libraries/graphic-types/src/graphic/mod.rs +++ b/node-graph/libraries/graphic-types/src/graphic/mod.rs @@ -8,7 +8,7 @@ pub use glue::{map_groups_to_owned, map_groups_to_persistent, map_groups_to_resi 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, PaintOverlay, PaintOverlayColumn, PaintReach, bake_paint_transforms, has_paint, is_paint_present, paint_graphics, set_paint_attribute, set_paint_attribute_at, + 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 walk::{GraphicLevel, GraphicLevelColumn, RowStep, VectorRow, direct_vector_len, flatten_vector_rows, group_is_empty, lane_attributes, run_lane_attributes, walk_vector_rows}; @@ -399,9 +399,9 @@ impl<'e> Graphic<'e> { } } - pub fn can_reduce_to_clip_path(&self) -> bool { + pub fn can_reduce_to_clip_path(&self, inherited_appearance: Option<&Appearance>) -> bool { match self { - Graphic::Vector(vector) => vector_can_reduce_to_clip_path(&core_types::lane::Single(vector)), + Graphic::Vector(vector) => vector_can_reduce_to_clip_path(&core_types::lane::Single(vector), inherited_appearance), _ => false, } } diff --git a/node-graph/libraries/graphic-types/src/graphic/paint.rs b/node-graph/libraries/graphic-types/src/graphic/paint.rs index 1200a69a2d..ad9c8dfeab 100644 --- a/node-graph/libraries/graphic-types/src/graphic/paint.rs +++ b/node-graph/libraries/graphic-types/src/graphic/paint.rs @@ -40,15 +40,26 @@ where /// 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) -> bool { +pub fn vector_can_reduce_to_clip_path>(source: &S, inherited_appearance: Option<&Appearance>) -> bool { (0..source.lane_count()).all(|index| { - let Some(element) = source.element(index) else { return false }; + if source.element(index).is_none() { + return false; + } let opacity: f64 = source.attr::(index); - let fill_opaque_or_absent = paint_graphics::(source, index).is_none_or(|graphic_list| graphic_list.element(0).is_none_or(|graphic| graphic.is_opaque())); + let appearance = Appearance::cascade(source.attr::(index), inherited_appearance); + let resolved = appearance.map(Appearance::fill_and_stroke).unwrap_or_default(); - let stroke_invisible_or_transparent = element.stroke.as_ref().is_none_or(|stroke| !stroke.has_renderable_stroke()) - || paint_graphics::(source, index).is_none_or(|graphic_list| graphic_list.element(0).is_none_or(|graphic| graphic.is_fully_transparent())); + let fill_opaque_or_absent = resolved + .fill_paint + .and_then(paint_cell_rows) + .is_none_or(|graphic_list| graphic_list.element(0).is_none_or(|graphic| graphic.is_opaque())); + + let stroke_invisible_or_transparent = resolved.stroke.as_ref().is_none_or(|stroke| !stroke.has_renderable_stroke()) + || resolved + .stroke_paint + .and_then(paint_cell_rows) + .is_none_or(|graphic_list| graphic_list.element(0).is_none_or(|graphic| graphic.is_fully_transparent())); opacity > 1. - f64::EPSILON && fill_opaque_or_absent && stroke_invisible_or_transparent }) @@ -168,76 +179,13 @@ impl<'a> PaintReach<'a> { } } -/// A source with a lane's paint forced over its fill and stroke columns, -/// reaching the interiors the legacy conversion's paint push reached. -pub struct PaintOverlay<'a, S> { - inner: &'a S, - paint: LanePaint<'a>, -} - -impl<'a, S> PaintOverlay<'a, S> { - pub fn new(inner: &'a S, paint: LanePaint<'a>) -> Self { - Self { inner, paint } - } -} - -pub struct PaintOverlayColumn<'a, S: LaneSource + 'a, A: Attribute> { - inner: S::Column<'a, A>, - forced: Option>, -} - -impl<'a, S: LaneSource, A: Attribute> LaneColumn<'a, A> for PaintOverlayColumn<'a, S, A> { - fn try_get(&self, lane: usize) -> Option> { - match self.forced { - Some(forced) => Some(forced), - None => self.inner.try_get(lane), - } - } -} - -/// The forced value for the marker `A`: the lane paint where `A` is this -/// crate's fill or stroke marker, absent otherwise. -fn forced_paint<'a, A: Attribute>(paint: LanePaint<'a>) -> Option> { - let slot = match A::NAME { - name if name == Fill::NAME => paint.fill, - name if name == Stroke::NAME => paint.stroke, +/// The paint a coverage row's cell holds, in the canonical `List` form the paint +/// renderers consume: this crate's writers carry the list as one graphic cell, and a bare +/// cell of any other form is treated as paint that draws nothing. +pub fn paint_cell_rows<'a>(cell: &'a Graphic<'static>) -> Option<&'a List>> { + match cell { + Graphic::Graphic(list) => Some(list).filter(|list| is_paint_present(list)), _ => None, - }?; - assert_eq!( - std::any::TypeId::of::>(), - std::any::TypeId::of::>>>(), - "attribute `{}` is declared at another value type than this crate's paint form", - A::NAME - ); - assert_eq!( - size_of::>(), - size_of::>>>(), - "the paint value form must span the marker's value" - ); - // SAFETY: the census admits one value type per attribute name, so a `fill` or `stroke` marker carries this crate's `Option<&List>` at the asserted size. - Some(unsafe { std::mem::transmute_copy::>, A::Value<'a>>(&Some(slot)) }) -} - -impl<'a, S: LaneSource> LaneSource for PaintOverlay<'a, S> { - type Element = S::Element; - type Column<'b, A: Attribute> - = PaintOverlayColumn<'b, S, A> - where - Self: 'b; - - fn lane_count(&self) -> usize { - self.inner.lane_count() - } - - fn element(&self, lane: usize) -> Option<&S::Element> { - self.inner.element(lane) - } - - fn column(&self) -> PaintOverlayColumn<'_, S, A> { - PaintOverlayColumn { - inner: self.inner.column::(), - forced: forced_paint::(self.paint), - } } } diff --git a/node-graph/libraries/rendering/src/renderer.rs b/node-graph/libraries/rendering/src/renderer.rs index c79e3715e9..3d02a85bcf 100644 --- a/node-graph/libraries/rendering/src/renderer.rs +++ b/node-graph/libraries/rendering/src/renderer.rs @@ -23,8 +23,9 @@ use dyn_any::DynAny; use glam::{DAffine2, DMat2, DVec2}; use graphene_hash::CacheHashWrapper; use graphene_resource::Resource; -use graphic_types::graphic::{PaintColumns, PaintOverlay, PaintReach, has_paint, is_paint_present, paint_graphics, set_paint_attribute, vector_can_reduce_to_clip_path}; -use graphic_types::markers::{EditorMergedLayers, Fill, Stroke}; +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::markers::{Appearance as AppearanceMarker, EditorMergedLayers}; use graphic_types::raster_types::{BitmapMut, CPU, GPU, Image, Raster, Texture}; use graphic_types::vector_types::gradient::{GradientStops, GradientType}; use graphic_types::vector_types::markers::{GradientType as GradientTypeAttr, SpreadMethod}; @@ -207,6 +208,11 @@ pub struct RenderContext { pub resource_overrides: Vec<(peniko::ImageBrush, Texture)>, } +/// The single black-fill appearance a mask clone paints with, at full alpha so the mask fully covers the interior. +fn black_fill_appearance() -> Appearance { + Appearance::new_single(Coverage::new_fill(), Graphic::Graphic(List::new_from_element(Graphic::Color(Color::BLACK)))) +} + /// The alpha multiplier a paint row's opacity attributes apply when it serves as a paint. /// Fill opacity fades a paint just as opacity does, but a masker drops it so it cannot reach the content clipped to it. pub(crate) fn paint_row_opacity(list: &List, index: usize, for_mask: bool) -> f32 { @@ -624,7 +630,7 @@ impl Render for Graphic<'_> { fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) { match self { Graphic::Graphic(list) => list.render_svg(render, render_params), - Graphic::Vector(vector) => render_vector_svg(&Single(vector), render, render_params), + Graphic::Vector(vector) => render_vector_svg(&Single(vector), None, render, render_params), Graphic::RasterCPU(raster) => render_raster_cpu_svg(&Single(raster), render, render_params), Graphic::RasterGPU(_) => (), Graphic::Color(color) => render_color_svg(&Single(color), render, render_params), @@ -637,7 +643,7 @@ impl Render for Graphic<'_> { fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2, context: &mut RenderContext, render_params: &RenderParams) { match self { Graphic::Graphic(list) => list.render_to_vello(scene, transform, context, render_params), - Graphic::Vector(vector) => render_vector_vello(&Single(vector), scene, transform, context, render_params), + Graphic::Vector(vector) => render_vector_vello(&Single(vector), None, scene, transform, context, render_params), Graphic::RasterCPU(raster) => render_raster_cpu_vello(&Single(raster), scene, transform, render_params), Graphic::RasterGPU(raster) => render_raster_gpu_vello(&Single(raster), scene, transform, context, render_params), Graphic::Color(color) => render_color_vello(&Single(color), scene, render_params), @@ -677,7 +683,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) if reach.applies() => render_vector_svg(&PaintOverlay::new(&Single(vector), reach.paint), render, render_params), + 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::Group(group) => render_group_svg(group, reach, render, render_params), _ => element.render_svg(render, render_params), @@ -686,7 +692,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) if reach.applies() => render_vector_vello(&PaintOverlay::new(&Single(vector), reach.paint), scene, transform, context, render_params), + 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::Group(group) => render_group_vello(group, reach, scene, transform, context, render_params), _ => element.render_to_vello(scene, transform, context, render_params), @@ -695,13 +701,12 @@ fn render_element_vello<'a>(element: &'a Graphic, reach: PaintReach<'a>, scene: fn element_can_reduce_to_clip_path<'a>(element: &'a Graphic, reach: PaintReach<'a>) -> bool { match element { - Graphic::Vector(vector) if reach.applies() => vector_can_reduce_to_clip_path(&PaintOverlay::new(&Single(vector), reach.paint)), + Graphic::Vector(vector) => vector_can_reduce_to_clip_path(&Single(vector), reach.appearance), Graphic::Group(group) => match RunView::::new(&group.content) { - Some(run) if reach.applies() => vector_can_reduce_to_clip_path(&PaintOverlay::new(&run, reach.paint)), - Some(run) => vector_can_reduce_to_clip_path(&run), + Some(run) => vector_can_reduce_to_clip_path(&run, reach.appearance), None => false, }, - _ => element.can_reduce_to_clip_path(), + _ => element.can_reduce_to_clip_path(reach.appearance), } } @@ -732,8 +737,7 @@ fn collect_element_metadata<'a>( match element { Graphic::Graphic(list) => collect_graphic_metadata_with(list, reach.nested(), metadata, footprint, element_id), - Graphic::Vector(vector) if reach.applies() => collect_vector_metadata(&PaintOverlay::new(&Single(vector), reach.paint), metadata, footprint, element_id), - Graphic::Vector(vector) => collect_vector_metadata(&Single(vector), 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), Graphic::Color(_) => {} @@ -775,8 +779,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::Vector(vector) if reach.applies() => add_vector_upstream_click_targets(&PaintOverlay::new(&Single(vector), reach.paint), click_targets), - Graphic::Vector(vector) => add_vector_upstream_click_targets(&Single(vector), 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(_) => {} Graphic::Text(text) => add_text_upstream_click_targets(&Single(text), click_targets), @@ -787,8 +790,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::Vector(vector) if reach.applies() => add_vector_upstream_outline_targets(&PaintOverlay::new(&Single(vector), reach.paint), outlines), - Graphic::Vector(vector) => add_vector_upstream_outline_targets(&Single(vector), 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(_) => {} Graphic::Text(text) => add_text_upstream_click_targets(&Single(text), outlines), @@ -803,10 +805,7 @@ fn render_group_svg<'a>(group: &'a Group, reach: PaintReach<'a>, render: &mut Sv if let Some(run) = RunView::::new(item) { render_graphic_svg_with(&run, reach.into_group_graphics(), render, render_params) } else if let Some(run) = RunView::::new(item) { - match reach.applies() { - true => render_vector_svg(&PaintOverlay::new(&run, reach.paint), render, render_params), - false => render_vector_svg(&run, render, render_params), - } + render_vector_svg(&run, reach.appearance, render, render_params) } else if let Some(run) = RunView::>::new(item) { render_raster_cpu_svg(&run, render, render_params) } else if item.typed_lanes::>().is_some() { @@ -824,10 +823,7 @@ fn render_group_vello<'a>(group: &'a Group, reach: PaintReach<'a>, scene: &mut S if let Some(run) = RunView::::new(item) { render_graphic_vello_with(&run, reach.into_group_graphics(), scene, transform, context, render_params) } else if let Some(run) = RunView::::new(item) { - match reach.applies() { - true => render_vector_vello(&PaintOverlay::new(&run, reach.paint), scene, transform, context, render_params), - false => render_vector_vello(&run, scene, transform, context, render_params), - } + render_vector_vello(&run, reach.appearance, scene, transform, context, render_params) } else if let Some(run) = RunView::>::new(item) { render_raster_cpu_vello(&run, scene, transform, render_params) } else if let Some(run) = RunView::>::new(item) { @@ -849,10 +845,7 @@ fn collect_group_metadata<'a>(group: &'a Group, reach: PaintReach<'a>, metadata: if let Some(run) = RunView::::new(item) { collect_graphic_metadata_with(&run, reach.into_group_graphics(), metadata, footprint, element_id) } else if let Some(run) = RunView::::new(item) { - match reach.applies() { - true => collect_vector_metadata(&PaintOverlay::new(&run, reach.paint), metadata, footprint, element_id), - false => collect_vector_metadata(&run, metadata, footprint, element_id), - } + collect_vector_metadata(&run, reach.appearance, metadata, footprint, element_id) } else if let Some(run) = RunView::>::new(item) { collect_raster_metadata(&run, metadata, footprint, element_id) } else if let Some(run) = RunView::>::new(item) { @@ -868,10 +861,7 @@ fn add_group_upstream_click_targets<'a>(group: &'a Group, reach: PaintReach<'a>, if let Some(run) = RunView::::new(item) { add_graphic_upstream_click_targets_with(&run, reach.into_group_graphics(), click_targets) } else if let Some(run) = RunView::::new(item) { - match reach.applies() { - true => add_vector_upstream_click_targets(&PaintOverlay::new(&run, reach.paint), click_targets), - false => add_vector_upstream_click_targets(&run, click_targets), - } + add_vector_upstream_click_targets(&run, reach.appearance, click_targets) } else if item.typed_lanes::>().is_some() || item.typed_lanes::>().is_some() { add_raster_upstream_click_targets(click_targets) } else if let Some(run) = RunView::::new(item) { @@ -884,10 +874,7 @@ fn add_group_upstream_outline_targets<'a>(group: &'a Group, reach: PaintReach<'a if let Some(run) = RunView::::new(item) { add_graphic_upstream_outline_targets_with(&run, reach.into_group_graphics(), outlines) } else if let Some(run) = RunView::::new(item) { - match reach.applies() { - true => add_vector_upstream_outline_targets(&PaintOverlay::new(&run, reach.paint), outlines), - false => add_vector_upstream_outline_targets(&run, outlines), - } + add_vector_upstream_outline_targets(&run, reach.appearance, outlines) } else if item.typed_lanes::>().is_some() || item.typed_lanes::>().is_some() { add_raster_upstream_click_targets(outlines) } else if let Some(run) = RunView::::new(item) { @@ -1343,7 +1330,7 @@ impl Render for List> { } } -fn render_vector_svg>(source: &S, render: &mut SvgRender, render_params: &RenderParams) { +fn render_vector_svg>(source: &S, inherited_appearance: Option<&Appearance>, render: &mut SvgRender, render_params: &RenderParams) { for index in 0..source.lane_count() { let Some(vector) = source.element(index) else { continue }; let item_transform: DAffine2 = source.attr::(index); @@ -1351,8 +1338,17 @@ fn render_vector_svg>(source: &S, render: &mut S let opacity_attr: f64 = source.attr::(index); let opacity_fill_attr: f64 = source.attr::(index); + // The lane's paint: its own declared appearance, or the nearest ancestor's through the cascade + let appearance = Appearance::cascade(source.attr::(index), inherited_appearance); + let mut resolved = appearance.map(Appearance::fill_and_stroke).unwrap_or_default(); + // The paint order rides the coverage list's row order + if let Some(stroke) = &mut resolved.stroke { + stroke.paint_order = if resolved.stroke_below { PaintOrder::StrokeBelow } else { PaintOrder::StrokeAbove }; + } + let element_stroke = resolved.stroke.as_ref(); + // Only consider strokes with non-zero weight, since default strokes with zero weight would prevent assigning the correct stroke transform - let has_real_stroke = vector.stroke.as_ref().filter(|stroke| stroke.weight() > 0.); + let has_real_stroke = element_stroke.filter(|stroke| stroke.weight() > 0.); let set_stroke_transform = has_real_stroke.map(|stroke| stroke.transform).filter(|transform| transform_is_invertible(*transform)); let applied_stroke_transform = set_stroke_transform.unwrap_or(item_transform); let applied_stroke_transform = render_params.alignment_parent_transform.unwrap_or(applied_stroke_transform); @@ -1372,26 +1368,26 @@ fn render_vector_svg>(source: &S, render: &mut S path.push_str(bezpath.to_svg().as_str()); } - let mask_type = if vector.stroke.as_ref().map(|x| x.align) == Some(StrokeAlign::Inside) { + let mask_type = if element_stroke.map(|x| x.align) == Some(StrokeAlign::Inside) { MaskType::Clip } else { MaskType::Mask }; - let fill_graphic_list = paint_graphics::(source, index); + let fill_graphic_list = resolved.fill_paint.and_then(paint_cell_rows); let fill_graphic = fill_graphic_list.and_then(|l| l.element(0)); - let stroke_graphic_list = paint_graphics::(source, index); + let stroke_graphic_list = resolved.stroke_paint.and_then(paint_cell_rows); let stroke_graphic = stroke_graphic_list.and_then(|l| l.element(0)); let path_is_closed = vector.stroke_bezier_paths().all(|path| path.closed()); let can_draw_aligned_stroke = path_is_closed - && vector.stroke.as_ref().is_some_and(|stroke| stroke.has_renderable_stroke() && stroke.align.is_not_centered()) + && element_stroke.is_some_and(|stroke| stroke.has_renderable_stroke() && stroke.align.is_not_centered()) && stroke_graphic.is_some_and(|graphic| !graphic.is_fully_transparent()); let can_use_paint_order = !(fill_graphic.is_none_or(|graphic| !graphic.covers_opaquely()) || mask_type == MaskType::Clip); let needs_separate_alignment_fill = can_draw_aligned_stroke && !can_use_paint_order; - let wants_stroke_below = vector.stroke.as_ref().map(|s| s.paint_order) == Some(PaintOrder::StrokeBelow); + let wants_stroke_below = element_stroke.map(|s| s.paint_order) == Some(PaintOrder::StrokeBelow); let override_paint_order = can_draw_aligned_stroke && can_use_paint_order; let use_face_fill = vector.use_face_fill(); @@ -1418,6 +1414,7 @@ fn render_vector_svg>(source: &S, render: &mut S // 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); (id, mask_type, vector_item) @@ -1452,7 +1449,7 @@ fn render_vector_svg>(source: &S, render: &mut S if let Some((ref id, mask_type, ref vector_item)) = push_id { let mut svg = SvgRender::new(); vector_item.render_svg(&mut svg, &render_params.for_alignment(applied_stroke_transform)); - let stroke = vector.stroke.as_ref().unwrap(); + let stroke = element_stroke.unwrap(); // `push_id` is only `Some` when `can_draw_aligned_stroke`, which is gated on `path_is_closed` let (largest_scale, _) = singular_values(applied_stroke_transform); let inflation = stroke.max_aabb_inflation(true) * largest_scale; @@ -1479,9 +1476,7 @@ fn render_vector_svg>(source: &S, render: &mut S render_params.aligned_strokes = can_draw_aligned_stroke; render_params.override_paint_order = override_paint_order; - let stroke_shape_attribute = vector - .stroke - .as_ref() + let stroke_shape_attribute = element_stroke .map(|stroke| { if stroke_graphic_list.is_some_and(is_paint_present) { stroke.render(defs, item_transform, element_transform, applied_stroke_transform, bounds_matrix, &render_params, PaintTarget::Stroke) @@ -1492,7 +1487,7 @@ fn render_vector_svg>(source: &S, render: &mut S .unwrap_or_default(); // Need to avoid generating only paint attribute, otherwise SVG uses 1px width stroke as a fallback - let stroke_visible = vector.stroke.as_ref().is_some_and(|stroke| stroke.has_renderable_stroke()) && stroke_graphic.is_some_and(|g| !g.is_fully_transparent()); + let stroke_visible = element_stroke.is_some_and(|stroke| stroke.has_renderable_stroke()) && stroke_graphic.is_some_and(|g| !g.is_fully_transparent()); let stroke_attribute = if stroke_visible { stroke_graphic_list .map(|list| { @@ -1555,7 +1550,14 @@ fn render_vector_svg>(source: &S, render: &mut S } } -fn render_vector_vello>(source: &S, scene: &mut Scene, parent_transform: DAffine2, context: &mut RenderContext, render_params: &RenderParams) { +fn render_vector_vello>( + source: &S, + inherited_appearance: Option<&Appearance>, + scene: &mut Scene, + parent_transform: DAffine2, + context: &mut RenderContext, + render_params: &RenderParams, +) { for index in 0..source.lane_count() { use graphic_types::vector_types::vector; @@ -1565,7 +1567,20 @@ fn render_vector_vello>(source: &S, scene: &mut let opacity_attr: f64 = source.attr::(index); let opacity_fill_attr: f64 = source.attr::(index); let multiplied_transform = parent_transform * item_transform; - let has_real_stroke = element.stroke.as_ref().filter(|stroke| stroke.weight() > 0.); + + // The lane's paint: its own declared appearance, or the nearest ancestor's through the cascade + let appearance = Appearance::cascade(source.attr::(index), inherited_appearance); + let mut resolved = appearance.map(Appearance::fill_and_stroke).unwrap_or_default(); + // The paint order rides the coverage list's row order + if let Some(stroke) = &mut resolved.stroke { + stroke.paint_order = if resolved.stroke_below { + vector::style::PaintOrder::StrokeBelow + } else { + vector::style::PaintOrder::StrokeAbove + }; + } + + let has_real_stroke = resolved.stroke.as_ref().filter(|stroke| stroke.weight() > 0.); let set_stroke_transform = has_real_stroke.map(|stroke| stroke.transform).filter(|transform| transform_is_invertible(*transform)); let mut applied_stroke_transform = set_stroke_transform.unwrap_or(multiplied_transform); let mut element_transform = set_stroke_transform @@ -1589,8 +1604,8 @@ fn render_vector_vello>(source: &S, scene: &mut } } - let fill_graphic_list = paint_graphics::(source, index); - let stroke_graphic_list = paint_graphics::(source, index); + let fill_graphic_list = resolved.fill_paint.and_then(paint_cell_rows); + let stroke_graphic_list = resolved.stroke_paint.and_then(paint_cell_rows); // If we're using opacity or a blend mode, we need to push a layer let blend_mode = match render_params.render_mode { @@ -1602,7 +1617,7 @@ fn render_vector_vello>(source: &S, scene: &mut // Whether the renderer will engage the stroke-alignment compositing trick (non-Center align on a fully closed path). // Used by both the blend-layer clip rect inflation below (as `max_aabb_inflation`'s `path_is_closed` arg, equivalent here since // the function ignores the arg for Center align) and the `SrcIn`/`SrcOut` aligned-stroke branch further down. - let stroke = element.stroke.as_ref(); + let stroke = resolved.stroke.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 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()); @@ -1764,6 +1779,7 @@ fn render_vector_vello>(source: &S, scene: &mut // 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); let bounds = element.bounding_box_with_transform(multiplied_transform).unwrap_or(layer_bounds); @@ -1833,7 +1849,13 @@ fn render_vector_vello>(source: &S, scene: &mut } } -fn collect_vector_metadata>(source: &S, metadata: &mut RenderMetadata, footprint: Footprint, caller_element_id: Option) { +fn collect_vector_metadata>( + source: &S, + inherited_appearance: Option<&Appearance>, + metadata: &mut RenderMetadata, + footprint: Footprint, + caller_element_id: Option, +) { // Aggregate all items' targets per element_id so multi-item lists (e.g. 'Text' node with "Separate Glyphs" active) produce hit areas for every glyph. // Targets are baked relative to item 0's transform since `Graphic::collect_metadata` records that as `local_transforms[element_id]`. let item_zero_transform: DAffine2 = if source.lane_count() > 0 { source.attr::(0) } else { DAffine2::IDENTITY }; @@ -1852,6 +1874,10 @@ fn collect_vector_metadata>(source: &S, metadata let layer_path: &[NodeId] = source.attr::(index); let layer = layer_path.last().copied(); + // The lane's paint: its own declared appearance, or the nearest ancestor's through the cascade + let appearance = Appearance::cascade(source.attr::(index), inherited_appearance); + let resolved = appearance.map(Appearance::fill_and_stroke).unwrap_or_default(); + if let Some(element_id) = caller_element_id.or(layer) { // When recovering element_id from the item's editor:layer_path tag (because the caller // passed None), also store the transform metadata that Graphic::collect_metadata @@ -1867,12 +1893,12 @@ fn collect_vector_metadata>(source: &S, metadata let item_relative_transform = item_zero_inverse * transform; let mut click_targets_unwrapped = Vec::new(); - extend_targets_from_vector(&mut click_targets_unwrapped, source, index, click_target_vector, item_relative_transform); + extend_targets_from_vector(&mut click_targets_unwrapped, &resolved, click_target_vector, item_relative_transform); accumulated_click_targets.entry(element_id).or_default().extend(click_targets_unwrapped.into_iter().map(Arc::new)); // Outlines always use source geometry so the visual outline reflects actual letterforms let mut outlines_unwrapped = Vec::new(); - extend_targets_from_vector(&mut outlines_unwrapped, source, index, element, item_relative_transform); + extend_targets_from_vector(&mut outlines_unwrapped, &resolved, element, item_relative_transform); accumulated_outlines.entry(element_id).or_default().extend(outlines_unwrapped.into_iter().map(Arc::new)); // Source geometry (not the click-target override) so editing tools work on letterforms. @@ -1882,10 +1908,10 @@ fn collect_vector_metadata>(source: &S, metadata if let std::collections::hash_map::Entry::Vacant(e) = metadata.vector_data.entry(element_id) { e.insert(Arc::new(element.clone())); - if let Some(fill_graphic) = source.attr::(index).filter(|list| is_paint_present(list)) { + if let Some(fill_graphic) = resolved.fill_paint.and_then(paint_cell_rows) { metadata.fill_attributes.insert(element_id, Arc::new(fill_graphic.clone())); } - if let Some(stroke_graphic) = source.attr::(index).filter(|list| is_paint_present(list)) { + if let Some(stroke_graphic) = resolved.stroke_paint.and_then(paint_cell_rows) { metadata.stroke_attributes.insert(element_id, Arc::new(stroke_graphic.clone())); } } @@ -1915,7 +1941,7 @@ fn collect_vector_metadata>(source: &S, metadata } } -fn add_vector_upstream_click_targets>(source: &S, click_targets: &mut Vec) { +fn add_vector_upstream_click_targets>(source: &S, inherited_appearance: Option<&Appearance>, click_targets: &mut Vec) { for index in 0..source.lane_count() { let Some(element) = source.element(index) else { continue }; let transform: DAffine2 = source.attr::(index); @@ -1923,39 +1949,45 @@ fn add_vector_upstream_click_targets>(source: &S // Use click-target override geometry if the item provides one (e.g. 'Text' node's per-glyph bounding boxes) let vector = source.attr::(index).unwrap_or(element); - extend_targets_from_vector(click_targets, source, index, vector, transform); + let appearance = Appearance::cascade(source.attr::(index), inherited_appearance); + let resolved = appearance.map(Appearance::fill_and_stroke).unwrap_or_default(); + + extend_targets_from_vector(click_targets, &resolved, vector, transform); } } -fn add_vector_upstream_outline_targets>(source: &S, outlines: &mut Vec) { +fn add_vector_upstream_outline_targets>(source: &S, inherited_appearance: Option<&Appearance>, outlines: &mut Vec) { // Source geometry only, ignoring `editor:click_target`, so outlines reflect actual letterforms for index in 0..source.lane_count() { let Some(element) = source.element(index) else { continue }; let transform: DAffine2 = source.attr::(index); - extend_targets_from_vector(outlines, source, index, element, transform); + let appearance = Appearance::cascade(source.attr::(index), inherited_appearance); + let resolved = appearance.map(Appearance::fill_and_stroke).unwrap_or_default(); + + extend_targets_from_vector(outlines, &resolved, element, transform); } } impl Render for List { fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) { - render_vector_svg(self, render, render_params) + render_vector_svg(self, None, render, render_params) } fn render_to_vello(&self, scene: &mut Scene, parent_transform: DAffine2, context: &mut RenderContext, render_params: &RenderParams) { - render_vector_vello(self, scene, parent_transform, context, render_params) + render_vector_vello(self, None, scene, parent_transform, context, render_params) } fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, caller_element_id: Option) { - collect_vector_metadata(self, metadata, footprint, caller_element_id) + collect_vector_metadata(self, None, metadata, footprint, caller_element_id) } fn add_upstream_click_targets(&self, click_targets: &mut Vec) { - add_vector_upstream_click_targets(self, click_targets) + add_vector_upstream_click_targets(self, None, click_targets) } fn add_upstream_outline_targets(&self, outlines: &mut Vec) { - add_vector_upstream_outline_targets(self, outlines) + add_vector_upstream_outline_targets(self, None, outlines) } fn new_ids_from_hash(&mut self, reference: Option) { @@ -1967,15 +1999,15 @@ impl Render for List { /// Build one `CompoundPath` (non-zero fill rule, so holes like the inside of an "O" work /// correctly) plus one `FreePoint` per disconnected anchor, apply the transform, and append. -fn extend_targets_from_vector>(targets: &mut Vec, source: &S, index: usize, geometry: &Vector, transform: DAffine2) { - let filled = has_paint::(source, index); +fn extend_targets_from_vector(targets: &mut Vec, resolved: &graphic_types::appearance::FillAndStroke<'_>, geometry: &Vector, transform: DAffine2) { + let filled = resolved.fill_paint.and_then(paint_cell_rows).is_some(); let mut subpaths: Vec> = geometry.stroke_bezier_paths().collect(); let all_subpaths_closed = subpaths.iter().all(|subpath| subpath.closed()); // Inside/Outside-aligned strokes reach `weight` from the centerline rather than `weight / 2` per side, // so they need double the click inflation. Alignment is only honored by the renderer for fully-closed paths. - let stroke_width = geometry.stroke.as_ref().map_or(0., |stroke| { + let stroke_width = resolved.stroke.as_ref().map_or(0., |stroke| { if stroke.align.is_not_centered() && all_subpaths_closed { stroke.weight * 2. } else { @@ -2954,23 +2986,23 @@ impl Render for RunView<'_, Graphic<'_>> { impl Render for RunView<'_, Vector> { fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) { - render_vector_svg(self, render, render_params) + render_vector_svg(self, None, render, render_params) } fn render_to_vello(&self, scene: &mut Scene, parent_transform: DAffine2, context: &mut RenderContext, render_params: &RenderParams) { - render_vector_vello(self, scene, parent_transform, context, render_params) + render_vector_vello(self, None, scene, parent_transform, context, render_params) } fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, caller_element_id: Option) { - collect_vector_metadata(self, metadata, footprint, caller_element_id) + collect_vector_metadata(self, None, metadata, footprint, caller_element_id) } fn add_upstream_click_targets(&self, click_targets: &mut Vec) { - add_vector_upstream_click_targets(self, click_targets) + add_vector_upstream_click_targets(self, None, click_targets) } fn add_upstream_outline_targets(&self, outlines: &mut Vec) { - add_vector_upstream_outline_targets(self, outlines) + add_vector_upstream_outline_targets(self, None, outlines) } } @@ -3120,6 +3152,11 @@ 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. + fn fill_appearance(paint: &List>) -> Appearance { + Appearance::new_single(Coverage::new_fill(), Graphic::Graphic(paint.clone())) + } + fn rendered_svg(render: impl FnOnce(&mut SvgRender)) -> (String, String) { let mut svg_render = SvgRender::new(); render(&mut svg_render); @@ -3132,9 +3169,11 @@ mod group_walk_tests { let paint = color_paint(); 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 mut builder = RunBuilder::new(&arena, element_write_hashed::(), &[FieldWrite::of::(0)], 2).unwrap(); + let appearance = fill_appearance(&paint); + let mut builder = RunBuilder::new(&arena, element_write_hashed::(), &[FieldWrite::of::(0), 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(); let group = Group { row: None, content: item }; @@ -3151,9 +3190,11 @@ mod group_walk_tests { let paint = color_paint(); let inner = Graphic::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 appearance = fill_appearance(&paint); + let mut builder = RunBuilder::new(&arena, element_write_hashed::(), &[FieldWrite::of::(0), 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 }; @@ -3170,9 +3211,11 @@ mod group_walk_tests { let paint = color_paint(); let vectors = [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 appearance = fill_appearance(&paint); + let mut builder = RunBuilder::new(&arena, element_write_hashed::(), &[FieldWrite::of::(0), 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 }; @@ -3195,9 +3238,11 @@ mod group_walk_tests { let paint = color_paint(); 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 mut builder = RunBuilder::new(&arena, element_write_hashed::(), &[FieldWrite::of::(0)], 2).unwrap(); + let appearance = fill_appearance(&paint); + let mut builder = RunBuilder::new(&arena, element_write_hashed::(), &[FieldWrite::of::(0), 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(); let group = Group { row: None, content: item }; diff --git a/node-graph/nodes/graphic/src/graphic.rs b/node-graph/nodes/graphic/src/graphic.rs index de7dba1917..6a02ddd7f1 100644 --- a/node-graph/nodes/graphic/src/graphic.rs +++ b/node-graph/nodes/graphic/src/graphic.rs @@ -183,6 +183,7 @@ fn mirror_lane<'e, T: Clone + Default + Send + Sync + '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>, Attr<'e, core_types::attribute::OpacityFill>, @@ -227,6 +228,10 @@ where } 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, + }; let layer_path: Vec = legacy.attribute::>(ATTR_EDITOR_LAYER_PATH, source).cloned().unwrap_or_default(); let layer_path = arena.alloc(layer_path).ok_or_else(exhausted)?.0; @@ -235,6 +240,7 @@ where 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.)), Attr(legacy.attribute_cloned_or(core_types::ATTR_OPACITY_FILL, source, 1.)), @@ -268,6 +274,7 @@ fn mirror<'e>( 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>, Attr<'e, core_types::attribute::OpacityFill>, @@ -318,6 +325,7 @@ fn mirror_vector<'e>( 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>, Attr<'e, core_types::attribute::OpacityFill>, diff --git a/node-graph/nodes/path-bool/src/lib.rs b/node-graph/nodes/path-bool/src/lib.rs index 70cfef21c1..3a17f26f83 100644 --- a/node-graph/nodes/path-bool/src/lib.rs +++ b/node-graph/nodes/path-bool/src/lib.rs @@ -3,8 +3,9 @@ use core_types::list::{Item, List}; 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, CoverPlacement, 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::{EditorMergedLayers, Fill, Stroke}; +use graphic_types::markers::{Appearance as AppearanceMarker, EditorMergedLayers, Fill, Stroke}; use graphic_types::raster_types::{CPU, GPU, Raster}; use graphic_types::vector_types::GradientStops; use graphic_types::vector_types::gradient::{GradientSpreadMethod, GradientType}; @@ -35,6 +36,7 @@ fn boolean_core<'e>( Attr<'e, TransformAttr>, Attr<'e, Fill>, Attr<'e, Stroke>, + Attr<'e, AppearanceMarker>, Attr<'e, BlendModeAttr>, Attr<'e, Opacity>, Attr<'e, OpacityFill>, @@ -78,6 +80,10 @@ fn boolean_core<'e>( 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, + }; let layer_path: Vec = result_vector_list.attribute::>(ATTR_EDITOR_LAYER_PATH, 0).cloned().unwrap_or_default(); let layer_path = arena.alloc(layer_path).ok_or_else(exhausted)?.0; // Snapshot the input layers so the renderer can recurse into them for @@ -89,6 +95,7 @@ fn boolean_core<'e>( 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.)), Attr(result_vector_list.attribute_cloned_or(ATTR_OPACITY_FILL, 0, 1.)), @@ -117,6 +124,7 @@ fn boolean_operation<'e>( Attr<'e, TransformAttr>, Attr<'e, Fill>, Attr<'e, Stroke>, + Attr<'e, AppearanceMarker>, Attr<'e, BlendModeAttr>, Attr<'e, Opacity>, Attr<'e, OpacityFill>, @@ -144,6 +152,7 @@ fn boolean_operation_vector<'e>( Attr<'e, TransformAttr>, Attr<'e, Fill>, Attr<'e, Stroke>, + Attr<'e, AppearanceMarker>, Attr<'e, BlendModeAttr>, Attr<'e, Opacity>, Attr<'e, OpacityFill>, @@ -301,15 +310,22 @@ fn raster_stand_in_rows(image: &S, parent_trans .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. +fn fill_appearance(paint: List>) -> Appearance { + Appearance::new_single(Coverage::new_fill(), Graphic::Graphic(paint)) +} + /// 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(); element.set_stroke_transform(DAffine2::IDENTITY); @@ -330,7 +346,8 @@ 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)); + attributes.insert(ATTR_FILL, Some(gradient_paint.clone())); + attributes.insert(graphic_types::ATTR_APPEARANCE, fill_appearance(gradient_paint)); let mut element = Vector::default(); element.set_stroke_transform(DAffine2::IDENTITY); @@ -368,6 +385,7 @@ fn push_leaf_vector_row(out: &mut List, level: GraphicLevel<'_>, index: } } } + 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); } @@ -384,11 +402,21 @@ fn push_vector_rows(out: &mut List, rows: &List, composed: DAffi } } } + 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); } } +/// The cascade's resolved appearance lands on a row whose own is undeclared, since a declared row wins wholesale. +fn stamp_inherited_appearance(out: &mut List, index: usize, inherited: Option<&Appearance>) { + if let Some(appearance) = inherited + && out.attribute::(graphic_types::ATTR_APPEARANCE, index).and_then(Appearance::declared).is_none() + { + out.set_attribute(graphic_types::ATTR_APPEARANCE, index, appearance.clone()); + } +} + fn push_union(out: &mut List, flattened: List) { for row in boolean_operation_on_vector_list(&flattened, BooleanOperation::Union).into_iter() { out.push(row); diff --git a/node-graph/nodes/vector/src/vector_nodes.rs b/node-graph/nodes/vector/src/vector_nodes.rs index fc20f114a9..b881d6bfb0 100644 --- a/node-graph/nodes/vector/src/vector_nodes.rs +++ b/node-graph/nodes/vector/src/vector_nodes.rs @@ -17,7 +17,7 @@ use core_types::transform::Transform; use core_types::uuid::NodeId; use core_types::{ATTR_BLEND_MODE, ATTR_CLIPPING_MASK, ATTR_EDITOR_LAYER_PATH, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_TRANSFORM, CacheHash, Color, Ctx, DeriveCtx, ExtractIndex, InjectIndex}; use glam::{DAffine2, DMat2, DVec2}; -use graphic_types::appearance::{Appearance, CoverPlacement, Coverage}; +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::raster_types::{CPU, GPU, Raster}; @@ -101,7 +101,17 @@ 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, Attr<'e, Fill>, Attr<'e, StrokeAttr>, Attr<'e, EditorLayerPath>)>, Interrupt> { +) -> Result< + IList<( + Vector, + Attr<'e, TransformAttr>, + Attr<'e, Fill>, + Attr<'e, StrokeAttr>, + Attr<'e, AppearanceMarker>, + Attr<'e, EditorLayerPath>, + )>, + Interrupt, +> { let lane = ctx.index() as usize; if lane >= content.len() { return Err(GraphError::past_end().into()); @@ -110,11 +120,14 @@ fn assign_colors<'e>( 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; if gradient.is_empty() { - return Ok((element, transform, Attr(existing_fill), Attr(existing_stroke), layer_path)); + let parked_appearance = park_appearance_attr(existing_appearance)?; + return Ok((element, transform, Attr(existing_fill), Attr(existing_stroke), Attr(parked_appearance), layer_path)); } let gradient_element = gradient.element_ref(0); let reversed; @@ -138,7 +151,19 @@ fn assign_colors<'e>( true => Some(parked), false => existing_stroke, }; - Ok((element, transform, Attr(fill_attr), Attr(stroke_attr), layer_path)) + + // The same recolor lands on the appearance's coverage paints + let mut appearance = existing_appearance.unwrap_or_default(); + let paint_cell = Graphic::Graphic(parked.clone()); + if fill && !appearance.set_paint_of(Cover::Fill, paint_cell.clone()) { + appearance.replace_or_insert(Coverage::new_fill(), paint_cell.clone(), CoverPlacement::Above); + } + if stroke && element.stroke.is_some() && !appearance.set_paint_of(Cover::Stroke, paint_cell.clone()) { + appearance.replace_or_insert(Coverage::new_stroke(&element.stroke.clone().unwrap_or_default()), paint_cell, CoverPlacement::Above); + } + let parked_appearance = park_appearance_attr(Some(appearance))?; + + Ok((element, transform, Attr(fill_attr), Attr(stroke_attr), Attr(parked_appearance), layer_path)) } #[allow(clippy::too_many_arguments)] @@ -229,15 +254,26 @@ fn assign_colors_graphic<'e>( let element = match rows { Some(mut rows) => { for row in 0..rows.len() { - let has_stroke = rows.element(row).is_some_and(|vector| vector.stroke.is_some()); + 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 && has_stroke { + 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 + let mut appearance = rows.attribute_cloned_or_default::(graphic_types::ATTR_APPEARANCE, row); + let paint_cell = Graphic::Graphic(paint.clone()); + if fill && !appearance.set_paint_of(Cover::Fill, paint_cell.clone()) { + appearance.replace_or_insert(Coverage::new_fill(), paint_cell.clone(), CoverPlacement::Above); + } + if stroke && row_stroke.is_some() && !appearance.set_paint_of(Cover::Stroke, paint_cell.clone()) { + appearance.replace_or_insert(Coverage::new_stroke(&row_stroke.unwrap_or_default()), paint_cell, CoverPlacement::Above); + } + rows.set_attribute(graphic_types::ATTR_APPEARANCE, row, appearance); } let content = core_types::record::GroupItem::from_list(rows, ctx.arena()).ok_or_else(|| Interrupt::from(GraphError::new("the arena is exhausted")))?; Graphic::Group(core_types::record::Group { row: None, content }) @@ -287,6 +323,15 @@ fn park_paint<'e>(arena: &'e core_types::arena::Arena, paint: List(arena: &'e core_types::arena::Arena, attributes: &ItemAttributeValues) -> Result, Interrupt> { + attributes + .get::(graphic_types::ATTR_APPEARANCE) + .cloned() + .map(|appearance| park_appearance(arena, appearance)) + .transpose() +} + /// 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 { @@ -297,11 +342,12 @@ fn park_appearance<'e>(arena: &'e core_types::arena::Arena, appearance: Appearan } /// Appends one coverage to the content's appearance following the painter's algorithm: -/// the most downstream paint node in the chain paints on top. The coverage's paint is the -/// canonical paint list carried as one graphic cell, the input lane's own envelope dropped. -fn stamped_appearance(content_appearance: Option<&Appearance>, coverage: Coverage, paint: &List>) -> Appearance { +/// the most downstream paint node in the chain paints on top unless it asks for the +/// below placement. The coverage's paint is the canonical paint list carried as one +/// graphic cell, the input lane's own envelope dropped. +fn stamped_appearance(content_appearance: Option<&Appearance>, coverage: Coverage, paint: &List>, placement: CoverPlacement) -> Appearance { let mut appearance = content_appearance.cloned().unwrap_or_default(); - appearance.replace_or_insert(coverage, Graphic::Graphic(paint.clone()), CoverPlacement::Above); + appearance.replace_or_insert(coverage, Graphic::Graphic(paint.clone()), placement); appearance } @@ -363,7 +409,7 @@ fn fill<'e>( ) -> Result<(Vector, Attr<'e, Fill>, 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); + 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)))) @@ -390,7 +436,7 @@ 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); + 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)))) @@ -439,11 +485,10 @@ fn stroke<'e>( paint_order, }; - // 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. + // The coverage records the stroke's authoring space, so the item transform is composed in, translation + // included so the render consumers see the exact legacy stroke space. let mut coverage_stroke = stroke.clone(); coverage_stroke.transform *= *content_transform; - coverage_stroke.transform.translation = DVec2::ZERO; stroke.transform *= *content_transform; @@ -451,7 +496,12 @@ fn stroke<'e>( element.stroke = Some(stroke); let paint = paint_table(paint); - let appearance = stamped_appearance(*content_appearance, Coverage::new_stroke(&coverage_stroke), &paint); + // The coverage order carries the paint order: a below stroke lands before the fill in the list + let placement = match paint_order { + PaintOrder::StrokeAbove => CoverPlacement::Above, + PaintOrder::StrokeBelow => CoverPlacement::Below, + }; + let appearance = stamped_appearance(*content_appearance, Coverage::new_stroke(&coverage_stroke), &paint, placement); 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)))) @@ -506,11 +556,9 @@ fn stroke_graphic_leveled<'e>( paint_order, }; - // The coverage records the stroke's authoring space at the lane, composing the lane transform with cleared - // translation, for the same reason as in `stroke` above. + // The coverage records the stroke's authoring space at the lane, composing the lane transform as in `stroke` above. let mut coverage_stroke = stroke.clone(); coverage_stroke.transform *= *content_transform; - coverage_stroke.transform.translation = DVec2::ZERO; let mut element = element; for_each_interior_vector_mut(&mut element, |vector, transform| { @@ -520,7 +568,12 @@ fn stroke_graphic_leveled<'e>( }); let paint = paint_table(paint); - let appearance = stamped_appearance(*content_appearance, Coverage::new_stroke(&coverage_stroke), &paint); + // The coverage order carries the paint order: a below stroke lands before the fill in the list + let placement = match paint_order { + PaintOrder::StrokeAbove => CoverPlacement::Above, + PaintOrder::StrokeBelow => CoverPlacement::Below, + }; + let appearance = stamped_appearance(*content_appearance, Coverage::new_stroke(&coverage_stroke), &paint, placement); 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)))) @@ -1467,6 +1520,9 @@ fn solidify_rows(flattened: List) -> List { 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); + } Item::from_parts(vector, fill_attributes) }); @@ -1474,6 +1530,14 @@ fn solidify_rows(flattened: List) -> List { // 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(); + *appearance = Appearance::default(); + if let Some(paint) = stroke_paint { + appearance.replace_or_insert(Coverage::new_fill(), paint, CoverPlacement::Above); + } + } let stroke_row = Item::from_parts(solidified_stroke, stroke_attributes); @@ -1503,6 +1567,7 @@ fn solidify_native_lane<'e>( Attr<'e, TransformAttr>, Attr<'e, Fill>, Attr<'e, StrokeAttr>, + Attr<'e, AppearanceMarker>, Attr<'e, BlendModeAttr>, Attr<'e, Opacity>, Attr<'e, OpacityFill>, @@ -1563,6 +1628,7 @@ fn emit_legacy_lane<'e>( Attr<'e, TransformAttr>, Attr<'e, Fill>, Attr<'e, StrokeAttr>, + Attr<'e, AppearanceMarker>, Attr<'e, BlendModeAttr>, Attr<'e, Opacity>, Attr<'e, OpacityFill>, @@ -1593,6 +1659,11 @@ fn emit_legacy_lane<'e>( .and_then(|paint| paint.as_ref()) .map(|paint| park_paint(arena, paint.clone())) .transpose()?; + let appearance = output + .attribute::(graphic_types::ATTR_APPEARANCE, lane) + .cloned() + .map(|appearance| park_appearance(arena, appearance)) + .transpose()?; let layer_path: Vec = output.attribute::>(ATTR_EDITOR_LAYER_PATH, lane).cloned().unwrap_or_default(); let layer_path = arena.alloc(layer_path).ok_or_else(exhausted)?.0; let merged_layers = output @@ -1606,6 +1677,7 @@ fn emit_legacy_lane<'e>( 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.)), Attr(output.attribute_cloned_or(ATTR_OPACITY_FILL, lane, 1.)), @@ -1653,6 +1725,7 @@ fn solidify_stroke<'e>( Attr<'e, TransformAttr>, Attr<'e, Fill>, Attr<'e, StrokeAttr>, + Attr<'e, AppearanceMarker>, Attr<'e, BlendModeAttr>, Attr<'e, Opacity>, Attr<'e, OpacityFill>, @@ -1693,6 +1766,7 @@ fn solidify_stroke_vector<'e>( Attr<'e, TransformAttr>, Attr<'e, Fill>, Attr<'e, StrokeAttr>, + Attr<'e, AppearanceMarker>, Attr<'e, BlendModeAttr>, Attr<'e, Opacity>, Attr<'e, OpacityFill>, @@ -1765,6 +1839,7 @@ fn separate_subpaths<'e>( Attr<'e, TransformAttr>, Attr<'e, Fill>, Attr<'e, StrokeAttr>, + Attr<'e, AppearanceMarker>, Attr<'e, BlendModeAttr>, Attr<'e, Opacity>, Attr<'e, OpacityFill>, @@ -1821,6 +1896,7 @@ fn map_points<'e>( Attr<'e, TransformAttr>, Attr<'e, Fill>, Attr<'e, StrokeAttr>, + Attr<'e, AppearanceMarker>, Attr<'e, BlendModeAttr>, Attr<'e, Opacity>, Attr<'e, OpacityFill>, @@ -1862,6 +1938,7 @@ fn flatten_path_core<'e>( Attr<'e, TransformAttr>, Attr<'e, Fill>, Attr<'e, StrokeAttr>, + Attr<'e, AppearanceMarker>, Attr<'e, EditorLayerPath>, Attr<'e, EditorMergedLayers>, ), @@ -1927,7 +2004,31 @@ 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; - Ok((output, Attr(DAffine2::IDENTITY), Attr(fill), Attr(stroke), Attr(layer_path.as_slice()), Attr(Some(merged_layers)))) + // The carried paints land on the appearance too, 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(stroke_paint) = stroke { + let coverage = Coverage::new_stroke(&output.stroke.clone().unwrap_or_default()); + appearance.replace_or_insert(coverage, Graphic::Graphic(stroke_paint.clone()), CoverPlacement::Above); + } + match appearance.declared().is_some() { + true => Some(park_appearance(arena, appearance)?), + false => None, + } + }; + + Ok(( + output, + Attr(DAffine2::IDENTITY), + Attr(fill), + Attr(stroke), + 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. @@ -1941,6 +2042,7 @@ pub fn flatten_path<'e>( Attr<'e, TransformAttr>, Attr<'e, Fill>, Attr<'e, StrokeAttr>, + Attr<'e, AppearanceMarker>, Attr<'e, EditorLayerPath>, Attr<'e, EditorMergedLayers>, ), @@ -1964,6 +2066,7 @@ pub fn flatten_path_vector<'e>( Attr<'e, TransformAttr>, Attr<'e, Fill>, Attr<'e, StrokeAttr>, + Attr<'e, AppearanceMarker>, Attr<'e, EditorLayerPath>, Attr<'e, EditorMergedLayers>, ), @@ -2289,6 +2392,7 @@ fn cut_path<'e>( Attr<'e, TransformAttr>, Attr<'e, Fill>, Attr<'e, StrokeAttr>, + Attr<'e, AppearanceMarker>, Attr<'e, BlendModeAttr>, Attr<'e, Opacity>, Attr<'e, OpacityFill>, @@ -3273,11 +3377,19 @@ 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 + let mut appearance = Appearance::default(); if let Some(fill) = fill_paint { - item.set_attribute(ATTR_FILL, Some(fill)); + 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)); + 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); + } + if appearance.declared().is_some() { + item.set_attribute(graphic_types::ATTR_APPEARANCE, appearance); } List::new_from_item(item) @@ -3300,6 +3412,7 @@ fn morph_lane<'e>( Attr<'e, TransformAttr>, Attr<'e, Fill>, Attr<'e, StrokeAttr>, + Attr<'e, AppearanceMarker>, Attr<'e, BlendModeAttr>, Attr<'e, Opacity>, Attr<'e, OpacityFill>, @@ -3340,6 +3453,7 @@ fn morph<'e>( Attr<'e, TransformAttr>, Attr<'e, Fill>, Attr<'e, StrokeAttr>, + Attr<'e, AppearanceMarker>, Attr<'e, BlendModeAttr>, Attr<'e, Opacity>, Attr<'e, OpacityFill>, @@ -3372,6 +3486,7 @@ fn morph_vector<'e>( Attr<'e, TransformAttr>, Attr<'e, Fill>, Attr<'e, StrokeAttr>, + Attr<'e, AppearanceMarker>, Attr<'e, BlendModeAttr>, Attr<'e, Opacity>, Attr<'e, OpacityFill>,