From 4c32032d345ba4d8254444425877a193f49dfd85 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sat, 31 Oct 2026 08:48:58 -0700 Subject: [PATCH] Part of #3602: a node thumbnail reports the size it is drawn at render_node_thumbnail_with draws into a context of size.clamp(1, MAX_CONTEXT_SIDE) pixels but reported width and height as the requested size, so a size of 0 gave a 0x0 image holding 4 bytes. The view was also built from the requested size, and the fit scale (size - 2) / extent was 0 or negative at sizes 1 and 2, so the art drew nothing or mirrored. The thumbnail now reports the side it draws, builds the view from it, and drops the 1-pixel margin when the side is too small to keep one. Test: node_thumbnail_reports_the_size_it_draws. --- crates/render/src/renderer.rs | 7 ++++--- crates/render/src/tests.rs | 16 ++++++++++++++++ 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/crates/render/src/renderer.rs b/crates/render/src/renderer.rs index 3593bf041..218ef0620 100644 --- a/crates/render/src/renderer.rs +++ b/crates/render/src/renderer.rs @@ -347,9 +347,10 @@ impl Renderer { self.check_plugins(); self.check_symbols(doc); let b = brush_fx::cull_bounds(&doc.symbol_boxes(), n)?; - let s = (size as f64 - 2.0) / b.width().max(b.height()).max(1e-6); - let view = Affine::translate((size as f64 / 2.0, size as f64 / 2.0)) * Affine::scale(s) * Affine::translate(-b.center().to_vec2()); let w = size.clamp(1, MAX_CONTEXT_SIDE) as u16; + // A 1-pixel margin on each side, unless the thumbnail is too small to keep one. + let s = (w as f64 - 2.0).max(1.0) / b.width().max(b.height()).max(1e-6); + let view = Affine::translate((w as f64 / 2.0, w as f64 / 2.0)) * Affine::scale(s) * Affine::translate(-b.center().to_vec2()); let mut ctx = single_threaded_context(w, w); if let Some(bg) = background { ctx.set_paint(peniko::Color::from_rgba8(bg[0], bg[1], bg[2], bg[3])); @@ -364,6 +365,6 @@ impl Renderer { self.draw_forced_arc(&mut ctx, &frame, &Arc::new(n.clone())); let pm = self.finish(&mut ctx, vello_cpu::RasterizerSettings::default()); self.end_frame(); - Some(Rendered { width: size, height: size, pixels: pm.data_as_u8_slice().to_vec() }) + Some(Rendered { width: w as u32, height: w as u32, pixels: pm.data_as_u8_slice().to_vec() }) } } diff --git a/crates/render/src/tests.rs b/crates/render/src/tests.rs index 643701fc7..e7782b813 100644 --- a/crates/render/src/tests.rs +++ b/crates/render/src/tests.rs @@ -869,6 +869,22 @@ fn node_thumbnail_applies_the_root_mask() { assert_eq!(img.pixel(75, 50), [255, 255, 255, 255]); } +/// #3602: a thumbnail reports the size it was drawn at, and one too small for its 1-pixel margin +/// still draws the art. +#[test] +fn node_thumbnail_reports_the_size_it_draws() { + let mut d = Document::new(100.0, 100.0); + let node = rect_node(&mut d, Rect::new(10.0, 10.0, 90.0, 90.0), Paint::solid(Color::rgb(1.0, 0.0, 0.0)), Paint::None, 0.0); + let mut r = Renderer::new(); + for size in 0..4 { + let img = r.render_node_thumbnail(&d, &node, size, None).unwrap(); + assert_eq!(img.pixels.len(), img.width as usize * img.height as usize * 4, "size {size}"); + assert_eq!(img.width, size.max(1), "size {size}"); + assert_eq!(img.height, size.max(1), "size {size}"); + assert!(img.pixels.chunks(4).any(|p| p[3] > 0), "size {size}: the art is drawn"); + } +} + #[test] fn opacity_mask_with_undecodable_image_leaves_out_object() { use vectorcraft_doc::{Effect, ImageBlob, ImageObject, NodeKind, OpacityMask};