From a1b8bff6cda2d6a1224049606ea092703ac48681 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Tue, 15 Sep 2026 18:25:14 +0200 Subject: [PATCH] Fix color and gradient list paints not rendering as stacks (#4439) --- .../libraries/rendering/src/render_ext.rs | 11 +++- .../libraries/rendering/src/renderer.rs | 51 +++++++++++++++++++ 2 files changed, 60 insertions(+), 2 deletions(-) diff --git a/node-graph/libraries/rendering/src/render_ext.rs b/node-graph/libraries/rendering/src/render_ext.rs index 20d8640275..691cf7f634 100644 --- a/node-graph/libraries/rendering/src/render_ext.rs +++ b/node-graph/libraries/rendering/src/render_ext.rs @@ -266,12 +266,19 @@ impl RenderExt for List> { 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. }; diff --git a/node-graph/libraries/rendering/src/renderer.rs b/node-graph/libraries/rendering/src/renderer.rs index c3522dd5bf..1a5093ceb6 100644 --- a/node-graph/libraries/rendering/src/renderer.rs +++ b/node-graph/libraries/rendering/src/renderer.rs @@ -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) -> List> { + 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>) -> 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>) -> (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::(), &[FieldWrite::of::(0)], 1).unwrap(); + let lane = builder.push(unit_square_at(DVec2::ZERO)).unwrap(); + builder.attr::(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, ¶ms)) + } + + #[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("