From e035b24010c759dc4b749ea3b05460c6495b9050 Mon Sep 17 00:00:00 2001 From: YohYamasaki Date: Thu, 2 Jul 2026 14:19:49 +0900 Subject: [PATCH 1/2] Fix SVG blend isolation for artboards and thumbnails --- editor/src/node_graph_executor/runtime.rs | 4 +- .../libraries/rendering/src/renderer.rs | 45 ++++++++++++------- 2 files changed, 31 insertions(+), 18 deletions(-) diff --git a/editor/src/node_graph_executor/runtime.rs b/editor/src/node_graph_executor/runtime.rs index d799b3817d..d0ef87392f 100644 --- a/editor/src/node_graph_executor/runtime.rs +++ b/editor/src/node_graph_executor/runtime.rs @@ -491,8 +491,8 @@ impl NodeRuntime { let mut render = SvgRender::new(); graphic.render_svg(&mut render, &render_params); - // And give the SVG a viewbox and outer ... wrapper tag - render.format_svg(bounds[0], bounds[1]); + // And give the SVG a viewbox and outer ... wrapper tag as isolation group to prevent blending against the background checker + render.format_svg_with_attributes(bounds[0], bounds[1], |attributes| attributes.push("style", "isolation: isolate;")); render.svg }; diff --git a/node-graph/libraries/rendering/src/renderer.rs b/node-graph/libraries/rendering/src/renderer.rs index 25e4dee031..feaa08e347 100644 --- a/node-graph/libraries/rendering/src/renderer.rs +++ b/node-graph/libraries/rendering/src/renderer.rs @@ -93,10 +93,20 @@ impl SvgRender { /// Add an outer `...` tag with a `viewBox` and the `` pub fn format_svg(&mut self, bounds_min: DVec2, bounds_max: DVec2) { + self.format_svg_with_attributes(bounds_min, bounds_max, |_| {}); + } + + /// Add an outer `...` tag with a `viewBox`, the ``, and the provided attributes. + pub fn format_svg_with_attributes(&mut self, bounds_min: DVec2, bounds_max: DVec2, attributes: impl FnOnce(&mut SvgRenderAttrs)) { let (x, y) = bounds_min.into(); let (size_x, size_y) = (bounds_max - bounds_min).into(); + + let mut attr_render = SvgRender::new(); + attributes(&mut SvgRenderAttrs(&mut attr_render)); + let attributes_str = attr_render.svg.to_svg_string(); + let svg_header = format!( - r#"{defs}"#, + r#"{defs}"#, defs = &self.svg_defs ); self.svg_defs = String::new(); @@ -708,23 +718,9 @@ impl Render for List { let Some(content) = self.element(index).map(Artboard::as_graphic_list) else { continue }; let (location, dimensions, background, clip) = read_artboard_attributes(self, index); - let x = location.x.min(location.x + dimensions.x); - let y = location.y.min(location.y + dimensions.y); let width = dimensions.x.abs(); let height = dimensions.y.abs(); - // Background - render.leaf_tag("rect", |attributes| { - attributes.push("fill", format!("#{}", SRGBA8::from(background).to_rgb_hex())); - if background.a() < 1. { - attributes.push("fill-opacity", ((background.a() * 1000.).round() / 1000.).to_string()); - } - attributes.push("x", x.to_string()); - attributes.push("y", y.to_string()); - attributes.push("width", width.to_string()); - attributes.push("height", height.to_string()); - }); - // Artwork render.parent_tag( // SVG group tag @@ -747,10 +743,27 @@ impl Render for List { ) .unwrap(); attributes.push("clip-path", selector); + } else { + // Keep blend behavior consistent between clipped and unclipped artboards. + // An SVG clip-path creates an isolated group, so blends inside it do not use external backdrops such as the checkerboard preview background. + // Without a clip-path, the group would otherwise blend against that backdrop, so we explicitly isolate the artboard group. + // TODO: Consider another way to clip artboards. + // With multiple transparent artboards, blending does not take colors from other artboards into account, which is inconsistent from the vello's behavior. + attributes.push("style", "isolation: isolate;"); } }, - // Artwork content |render| { + // Background + render.leaf_tag("rect", |attributes| { + attributes.push("fill", format!("#{}", SRGBA8::from(background).to_rgb_hex())); + if background.a() < 1. { + attributes.push("fill-opacity", ((background.a() * 1000.).round() / 1000.).to_string()); + } + attributes.push("width", width.to_string()); + attributes.push("height", height.to_string()); + }); + + // Artwork content let mut render_params = render_params.clone(); render_params.artboard_background = Some(background); content.render_svg(render, &render_params); From 7b1eefdb0e14797e9fec8500f571a788f54c21c7 Mon Sep 17 00:00:00 2001 From: YohYamasaki Date: Thu, 2 Jul 2026 15:24:04 +0900 Subject: [PATCH 2/2] Fix regression on negative artboard size Normal situation will not create an artboard with negative dimension as `create_artboard` normalizes it, but it is still better to keep it in the renderer. --- node-graph/libraries/rendering/src/renderer.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/node-graph/libraries/rendering/src/renderer.rs b/node-graph/libraries/rendering/src/renderer.rs index feaa08e347..ce5fd300e9 100644 --- a/node-graph/libraries/rendering/src/renderer.rs +++ b/node-graph/libraries/rendering/src/renderer.rs @@ -720,6 +720,8 @@ impl Render for List { let width = dimensions.x.abs(); let height = dimensions.y.abs(); + let local_x = dimensions.x.min(0.); + let local_y = dimensions.y.min(0.); // Artwork render.parent_tag( @@ -738,8 +740,7 @@ impl Render for List { write!( &mut attributes.0.svg_defs, - r##""##, - dimensions.x, dimensions.y, + r##""##, ) .unwrap(); attributes.push("clip-path", selector); @@ -755,6 +756,13 @@ impl Render for List { |render| { // Background render.leaf_tag("rect", |attributes| { + if local_x != 0. { + attributes.push("x", local_x.to_string()); + } + if local_y != 0. { + attributes.push("y", local_y.to_string()); + } + attributes.push("fill", format!("#{}", SRGBA8::from(background).to_rgb_hex())); if background.a() < 1. { attributes.push("fill-opacity", ((background.a() * 1000.).round() / 1000.).to_string());