Render: node renders and thumbnails follow the supersample and precise options

render_node_with and render_node_thumbnail_with ignored RenderOptions::supersample
and RenderOptions::precise, so the Asset Export tiles, which ask for both, were drawn
with the canvas's shape-by-shape edges and the fast render mode instead of the export's
look. A node render now supersamples Art Optimized edges as a page render does, and
picks the quality render mode when precise is set, for the art's offscreen groups too.
This commit is contained in:
dotCooCoo
2026-11-10 21:35:47 -08:00
parent acc6acfdb2
commit eaaf1155a8
2 changed files with 90 additions and 30 deletions
+53 -30
View File
@@ -258,8 +258,7 @@ impl Renderer {
};
// `reset` keeps the threshold of the previous render: set it every time.
ctx.set_aliasing_threshold(opts.anti_alias.threshold());
let render_mode = if opts.precise { vello_cpu::RenderMode::OptimizeQuality } else { vello_cpu::RenderMode::OptimizeSpeed };
self.raster = vello_cpu::RasterizerSettings { render_mode, ..Default::default() };
self.raster = raster_settings(opts);
if opts.artboards && !opts.outline {
// Paper: white on screen, no ink on the ink planes.
ctx.set_transform(f.view);
@@ -358,20 +357,59 @@ impl Renderer {
pub fn render_node_with(&mut self, doc: &Document, n: &Arc<Node>, w: u16, h: u16, view: Affine, opts: &RenderOptions) -> Option<Rendered> {
self.check_plugins(doc);
self.check_symbols(doc);
let inv = view.inverse();
let visible = inv.transform_rect_bbox(Rect::new(0.0, 0.0, w as f64, h as f64));
let visible = view.inverse().transform_rect_bbox(Rect::new(0.0, 0.0, w as f64, h as f64));
let pixels = self.node_pixels(doc, n, (w, h), view, visible, opts, false, 1);
Some(Rendered { width: w as u32, height: h as u32, pixels })
}
/// Node `n` drawn into `w`×`h` transparent pixels through `view`, culled to `visible` (document
/// space); `forced` draws it whatever its visibility and bounds. With [`RenderOptions::supersample`]
/// and Art Optimized edges the art is drawn with hard edges at several times the size and
/// averaged down, and [`RenderOptions::precise`] picks the quality render mode, as in a page
/// render ([`Self::render_as`], [`Self::draw_frame`]). `cell` is as in [`Self::render_as`].
#[allow(clippy::too_many_arguments)]
fn node_pixels(
&mut self,
doc: &Document,
n: &Arc<Node>,
(w, h): (u16, u16),
view: Affine,
visible: Rect,
opts: &RenderOptions,
forced: bool,
cell: u32,
) -> Vec<u8> {
if opts.supersample
&& opts.anti_alias == AntiAlias::Art
&& let Some(k) = supersample_factor(u32::from(w), u32::from(h))
{
let hard = RenderOptions { anti_alias: AntiAlias::None, supersample: false, ..opts.clone() };
// `supersample_factor` keeps each side within MAX_CONTEXT_SIDE, so it fits in a u16.
let (bw, bh) = (u32::from(w) * k, u32::from(h) * k);
let hairline = std::mem::replace(&mut self.hairline, f64::from(k));
let big = self.node_pixels(doc, n, (bw as u16, bh as u16), Affine::scale(f64::from(k)) * view, visible, &hard, forced, k);
self.hairline = hairline;
return downsample(&big, bw as usize, bh as usize, k as usize);
}
let px = 1.0 / view.determinant().abs().sqrt().max(1e-12);
let mut ctx = single_threaded_context(w, h);
ctx.set_aliasing_threshold(opts.anti_alias.threshold());
let symbols = doc.symbol_boxes().quick();
let frame = Frame { mt: false, doc, symbols: &symbols, view, visible, px, opts, ink: Ink::Display, output: view, plates: None };
let output = Affine::scale(1.0 / f64::from(cell.max(1))) * view;
let frame = Frame { mt: false, doc, symbols: &symbols, view, visible, px, opts, ink: Ink::Display, output, plates: None };
let raster = std::mem::replace(&mut self.raster, raster_settings(opts));
self.begin_frame();
(self.knockout, self.nested, self.backdrop, self.picture) = (false, 0, None, None);
self.clip_paths.clear();
self.draw_arc(&mut ctx, &frame, n);
let pm = self.finish(&mut ctx, vello_cpu::RasterizerSettings::default());
if forced {
self.draw_forced_arc(&mut ctx, &frame, n);
} else {
self.draw_arc(&mut ctx, &frame, n);
}
let pm = self.finish(&mut ctx, self.raster);
self.end_frame();
Some(Rendered { width: w as u32, height: h as u32, pixels: pm.data_as_u8_slice().to_vec() })
self.raster = raster;
pm.data_as_u8_slice().to_vec()
}
/// Render any node (also one outside the tree, e.g. opacity-mask art) fitted into
@@ -402,28 +440,7 @@ impl Renderer {
// 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);
ctx.set_aliasing_threshold(opts.anti_alias.threshold());
let symbols = doc.symbol_boxes().quick();
let frame = Frame {
mt: false,
doc,
symbols: &symbols,
view,
visible: b.inflate(1.0, 1.0),
px: 1.0 / s,
opts,
ink: Ink::Display,
output: view,
plates: None,
};
self.begin_frame();
(self.knockout, self.nested, self.backdrop, self.picture) = (false, 0, None, None);
self.clip_paths.clear();
self.draw_forced_arc(&mut ctx, &frame, &Arc::new(n.clone()));
let pm = self.finish(&mut ctx, vello_cpu::RasterizerSettings::default());
self.end_frame();
let mut pixels = pm.data_as_u8_slice().to_vec();
let mut pixels = self.node_pixels(doc, &Arc::new(n.clone()), (w, w), view, b.inflate(1.0, 1.0), opts, true, 1);
if let Some(bg) = background {
over_background(&mut pixels, bg);
}
@@ -431,6 +448,12 @@ impl Renderer {
}
}
/// The rasterizer settings for `opts`: the quality render mode when [`RenderOptions::precise`] is set.
fn raster_settings(opts: &RenderOptions) -> vello_cpu::RasterizerSettings {
let render_mode = if opts.precise { vello_cpu::RenderMode::OptimizeQuality } else { vello_cpu::RenderMode::OptimizeSpeed };
vello_cpu::RasterizerSettings { render_mode, ..Default::default() }
}
/// Premultiplied RGBA `pixels` drawn over the solid color `background` (`[r, g, b, a]`, straight).
pub(crate) fn over_background(pixels: &mut [u8], [r, g, b, a]: [u8; 4]) {
let alpha = u32::from(a);
+37
View File
@@ -952,6 +952,43 @@ fn node_renders_follow_the_anti_alias_option() {
assert!(drawn(&thumb) && !partial(&thumb), "render_node_thumbnail_with: every alpha is 0 or 255");
}
/// Node renders follow `supersample` and `precise` as a page render does: two rectangles that meet
/// at x = 50.37 leave no seam when supersampled, and a node render gives the pixels of a page
/// render of the same art with the same options.
#[test]
fn node_renders_follow_the_supersample_and_precise_options() {
let mut d = Document::new(100.0, 100.0);
let left = rect_node(&mut d, Rect::new(10.0, 10.0, 50.37, 90.0), Paint::solid(Color::BLACK), Paint::None, 0.0);
let right = rect_node(&mut d, Rect::new(50.37, 10.0, 90.0, 90.0), Paint::solid(Color::rgb(0.2, 0.6, 0.9)), Paint::None, 0.0);
// Blended at half opacity over both, so the group composites offscreen.
let mut over = rect_node(&mut d, Rect::new(30.0, 60.0, 70.0, 80.0), Paint::solid(Color::rgb(0.9, 0.7, 0.1)), Paint::None, 0.0);
over.opacity = 0.5;
over.blend = vectorcraft_color::BlendMode::Multiply;
let group = Node::group(d.alloc_id(), vec![Arc::new(left), Arc::new(right), Arc::new(over)]);
let l = d.layers[0].id;
d.insert(Some(l), 0, group.clone()).unwrap();
let node = Arc::new(group.clone());
let page = |opts: &RenderOptions| RenderOptions { background: None, artboards: false, ..opts.clone() };
let mut r = Renderer::new();
for (supersample, precise) in [(false, false), (true, false), (false, true), (true, true)] {
let opts = RenderOptions { supersample, precise, ..Default::default() };
let case = format!("supersample {supersample}, precise {precise}");
let img = r.render_node_with(&d, &node, 100, 100, Affine::IDENTITY, &opts).unwrap();
let whole = Renderer::new().render(&d, 100, 100, Affine::IDENTITY, &page(&opts));
assert!(img.pixels == whole.pixels, "{case}: render_node_with draws as a page render does");
// Pixel column 50 at row 50: the black rectangle covers 37% of it and the blue one the rest.
let seam = img.pixels[(50 * 100 + 50) * 4 + 3];
assert_eq!(seam == 255, supersample, "{case}: render_node_with seam alpha {seam}");
// A 37-pixel thumbnail draws 0.4375 pixels a point, the seam at x = 18.66.
let thumb = r.render_node_thumbnail_with(&d, &group, 37, None, &opts).unwrap();
let view = Affine::translate((18.5, 18.5)) * Affine::scale(0.4375) * Affine::translate((-50.0, -50.0));
let whole = Renderer::new().render(&d, 37, 37, view, &page(&opts));
assert!(thumb.pixels == whole.pixels, "{case}: the thumbnail draws as a page render does");
let seam = thumb.pixels[(18 * 37 + 18) * 4 + 3];
assert_eq!(seam == 255, supersample, "{case}: thumbnail seam alpha {seam}");
}
}
#[test]
fn opacity_mask_with_undecodable_image_leaves_out_object() {
use vectorcraft_doc::{Effect, ImageBlob, ImageObject, NodeKind, OpacityMask};