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.
This commit is contained in:
dotCooCoo
2026-11-09 17:15:41 -08:00
parent c7c6c12ca8
commit aca44ae14e
3 changed files with 16 additions and 3 deletions
+14 -1
View File
@@ -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::<Vec<_>>();
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");
}
}
}
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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<Rendered> {
self.render_node_thumbnail_with(doc, n, size, background, &RenderOptions::default())