From aca44ae14ec3685a6156711113edfc37edc2573b Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Mon, 9 Nov 2026 17:15:41 -0800 Subject: [PATCH] Render: the glow pan test checks fractional pans, and background docs say straight alpha a_coarser_glow_kept_whole_draws_after_a_pan_as_made_again compared only whole-pixel pans with a fresh render. At a fractional pan it now compares the pixels right of the rectangle, where only the glow draws, with a fresh render at the whole pixel nearest the pan (either neighbor on a half pixel). RenderOptions::background and render_node_thumbnail's background are straight-alpha colors, as over_background reads them; their docs said premultiplied. --- crates/render/src/fx/tests.rs | 15 ++++++++++++++- crates/render/src/options.rs | 2 +- crates/render/src/renderer.rs | 2 +- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/crates/render/src/fx/tests.rs b/crates/render/src/fx/tests.rs index 560a3675c..80f338736 100644 --- a/crates/render/src/fx/tests.rs +++ b/crates/render/src/fx/tests.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use serde_json::json; use vectorcraft_doc::color::{Color, Paint}; use vectorcraft_doc::{Appearance, Document, Effect, Node}; -use vectorcraft_geom::{Affine, Rect, shapes}; +use vectorcraft_geom::{Affine, Point, Rect, shapes}; use super::{DRAG_PIXEL_BUDGET, Drawn, PIXEL_BUDGET, drag_scale, filter_region, filter_region_within, pan_margin}; use crate::tests_fontchange::{fonts_steady, with_fonts_unchanged}; @@ -214,6 +214,19 @@ fn a_coarser_glow_kept_whole_draws_after_a_pan_as_made_again() { fresh.threads = 2; if pan.0.fract() == 0.0 && pan.1.fract() == 0.0 { assert!(panned.pixels == fresh.render(&d, size.0, size.1, xf, &opts).pixels, "blur {blur}, pan {pan:?}"); + } else { + // Right of the rectangle's soft edge the frame holds only the glow, which the kept raster + // places at the whole pixel nearest the pan, where a fresh render draws it too. A pan of + // half a pixel is as near to the pixels on both sides, so either one is accepted. + let right = (xf * Point::new(rect.x1, rect.y0)).x.ceil() as usize + 3; + let row = size.0 as usize * 4; + let glow = |p: &[u8]| p.chunks(row).flat_map(|r| r[right * 4..].to_vec()).collect::>(); + let nearest = |v: f64| if (v - v.trunc()).abs() == 0.5 { vec![v.floor(), v.ceil()] } else { vec![v.round()] }; + let found = nearest(pan.0).into_iter().flat_map(|x| nearest(pan.1).into_iter().map(move |y| (x, y))).any(|whole| { + let at = Affine::translate(whole) * base; + glow(&panned.pixels) == glow(&fresh.render(&d, size.0, size.1, at, &opts).pixels) + }); + assert!(found, "blur {blur}, pan {pan:?}: the glow is drawn at the whole pixel nearest the pan"); } } } diff --git a/crates/render/src/options.rs b/crates/render/src/options.rs index 93d0f07b9..8a152e11f 100644 --- a/crates/render/src/options.rs +++ b/crates/render/src/options.rs @@ -35,7 +35,7 @@ pub fn raster_size(region: Rect, scale: f64) -> Result<(u32, u32), String> { pub struct RenderOptions { /// Outline (wireframe) view: 1 px black paths, no paint. pub outline: bool, - /// Pasteboard colour behind everything (premultiplied RGBA8); `None` = transparent. + /// Pasteboard color behind everything (straight-alpha RGBA8); `None` = transparent. pub background: Option<[u8; 4]>, /// Draw artboards as white rectangles (screen view). Export sets this to false. pub artboards: bool, diff --git a/crates/render/src/renderer.rs b/crates/render/src/renderer.rs index f5bcecaaa..a5bfcbf26 100644 --- a/crates/render/src/renderer.rs +++ b/crates/render/src/renderer.rs @@ -375,7 +375,7 @@ impl Renderer { } /// Render any node (also one outside the tree, e.g. opacity-mask art) fitted into - /// `size`×`size` pixels, optionally over a solid premultiplied background. A symbol instance + /// `size`×`size` pixels, optionally over a solid straight-alpha background. A symbol instance /// is fitted by its symbol's art in `doc`. pub fn render_node_thumbnail(&mut self, doc: &Document, n: &Node, size: u32, background: Option<[u8; 4]>) -> Option { self.render_node_thumbnail_with(doc, n, size, background, &RenderOptions::default())