Fix color and gradient list paints not rendering as stacks (#4439)

This commit is contained in:
Keavon Chambers
2026-09-15 18:25:14 +02:00
committed by Dennis Kobert
parent 1b81de3536
commit a1b8bff6cd
2 changed files with 60 additions and 2 deletions

View File

@@ -266,12 +266,19 @@ impl RenderExt for List<Graphic<'_>> {
let composited = composite_paint_colors(self, |graphic| if let Graphic::Color(color) = graphic { Some(*color) } else { None }, render_params.for_mask);
render_color_paint(composited, target)
}
Some(Graphic::Gradient(gradient)) => {
// One gradient resolves to a paint server; stacking several needs them composited, which only the pattern below can do
Some(Graphic::Gradient(gradient)) if self.len() <= 1 => {
let gradient_id = render_gradient_paint(&core_types::lane::LeafLane::new(self, 0, gradient), svg_defs, item_transform, element_transform, render_params.for_mask);
format!(r##" {paint_attr}="url(#{gradient_id})""##)
}
Some(Graphic::None) => format!(r#" {paint_attr}="none""#),
Some(Graphic::Vector(_)) | Some(Graphic::RasterCPU(_)) | Some(Graphic::RasterGPU(_)) | Some(Graphic::GraphicList(_)) | Some(Graphic::Text(_)) | Some(Graphic::Group(_)) => {
Some(Graphic::Gradient(_))
| Some(Graphic::Vector(_))
| Some(Graphic::RasterCPU(_))
| Some(Graphic::RasterGPU(_))
| Some(Graphic::GraphicList(_))
| Some(Graphic::Text(_))
| Some(Graphic::Group(_)) => {
let bounds = if target == PaintTarget::Stroke {
// To prevent a wraparound artefact occurring when the tile boundary and the stroke region are perfectly aligned, the local coordinate is expanded slightly.
let inverse = |len: f64| if len > 0. { 1. / len } else { 0. };

View File

@@ -3460,6 +3460,26 @@ mod group_walk_tests {
List::new_from_element(Graphic::Color(Color::from_rgbaf32(0.8, 0.2, 0.33, 1.).unwrap()))
}
/// A paint holding one gradient row per color, each ramping that color to transparent.
fn gradient_paint(colors: impl IntoIterator<Item = Color>) -> List<Graphic<'static>> {
use graphic_types::vector_types::gradient::{Gradient, GradientStop};
colors
.into_iter()
.map(|color| {
let stops = [
GradientStop { position: 0., midpoint: 0.5, color },
GradientStop {
position: 1.,
midpoint: 0.5,
color: Color::TRANSPARENT,
},
];
Item::new_from_element(Graphic::Gradient(Gradient::new(stops)))
})
.collect()
}
/// The appearance the fill node stamps, so test content mirrors node output.
fn fill_appearance(paint: &List<Graphic<'static>>) -> Appearance {
Appearance::new_single(Coverage::new_fill(), Graphic::GraphicList(paint.clone()))
@@ -3571,6 +3591,37 @@ mod group_walk_tests {
assert_eq!(native_outlines, legacy);
}
/// Renders a unit square filled with `paint` and returns its SVG body and defs.
fn rendered_fill(paint: &List<Graphic<'static>>) -> (String, String) {
let arena = core_types::arena::Arena::new(1 << 16).unwrap();
let appearance = fill_appearance(paint);
let mut builder = RunBuilder::new(&arena, element_write_hashed::<Vector>(), &[FieldWrite::of::<AppearanceMarker>(0)], 1).unwrap();
let lane = builder.push(unit_square_at(DVec2::ZERO)).unwrap();
builder.attr::<AppearanceMarker>(lane, Some(&appearance));
let group = Group { row: None, content: builder.finish() };
let params = RenderParams::default();
rendered_svg(|render| Graphic::Group(group.clone()).render_svg(render, &params))
}
#[test]
fn a_lone_gradient_paint_renders_as_one_paint_server() {
let (svg, defs) = rendered_fill(&gradient_paint([Color::RED]));
assert!(svg.contains("fill=\"url(#"), "a single gradient should reference a paint server directly, got {svg}");
assert_eq!(defs.matches("<linearGradient").count(), 1, "the lone gradient should reach the defs, got {defs}");
assert!(!defs.contains("<pattern"), "a single gradient needs no pattern, got {defs}");
}
#[test]
fn stacked_gradient_paints_render_every_row() {
let (_svg, defs) = rendered_fill(&gradient_paint([Color::RED, Color::BLUE]));
// Two gradients cannot collapse into one paint server, so they must stack inside a pattern
assert!(defs.contains("<pattern"), "stacked gradients should render through a pattern, got {defs}");
assert_eq!(defs.matches("<linearGradient").count(), 2, "both gradient rows should reach the defs, got {defs}");
}
#[test]
fn stacked_paint_colors_composite_in_straight_alpha() {
// A half-transparent red over an opaque blue lands halfway between the two