Merge pull request #4787 from dotCooCoo/fix/pattern-undecoded-marks

Render: pattern tiles draw undecoded-image marks when the view does
This commit is contained in:
BFlat
2026-10-31 20:11:22 +07:00
committed by GitHub
3 changed files with 85 additions and 15 deletions
+20 -10
View File
@@ -4,7 +4,7 @@
//! ([`PatternDef::period`]) is rasterized at the device scale it is painted at, rounded up to a
//! power of two and capped ([`raster_scale`]), and used as an image paint with `Extend::Repeat`,
//! with the paint transform mapping pixels → pattern space → document. Tiles are cached per
//! pattern name and raster scale (and ink plane, see [`crate::ink`]) within
//! pattern name, raster scale, undecoded-image mark setting and ink plane (see [`crate::ink`]) within
//! [`lru::PATTERN_BUDGET`]. A tile is used while the pattern's definition is the one it was
//! rasterized from (art `Arc` identity, tile, tile type, overlap): the pattern's "revision", and,
//! when its art shows more than itself (a symbol, another pattern, an image, type, a brush), while
@@ -33,9 +33,9 @@ use crate::{Frame, RenderOptions, Rendered, Renderer};
/// Largest super-tile raster side (pixels).
const MAX_TILE_PX: f64 = 2048.0;
/// What a tile is kept under: the pattern's name and the bits of its raster scale
/// ([`raster_scale`]).
type TileKey = (String, u64, Ink);
/// What a tile is kept under: the pattern's name, the bits of its raster scale
/// ([`raster_scale`]), ink plane and undecoded-image mark setting.
type TileKey = (String, u64, Ink, bool);
/// What a tile was drawn from besides its pattern's own art, for art that shows more than itself:
/// the art of every pattern and symbol and the pixels of every image in the document, the
@@ -169,12 +169,20 @@ pub(crate) fn raster_scale(s: f64, period: (f64, f64)) -> f64 {
/// Rasterize `region` (pattern space) of the infinite tiling into a `w`×`h` pixmap, colours as
/// `ink` paints them.
fn rasterize(r: &mut Renderer, doc: &Document, def: &PatternDef, region: Rect, w: u16, h: u16, ink: Ink) -> (Pixmap, bool) {
let (w, h) = (w.max(1), h.max(1));
fn rasterize(
r: &mut Renderer,
doc: &Document,
def: &PatternDef,
region: Rect,
size: (u16, u16),
ink: Ink,
mark_undecoded_images: bool,
) -> (Pixmap, bool) {
let (w, h) = (size.0.max(1), size.1.max(1));
let mut ctx = crate::single_threaded_context(w, h);
let view = Affine::scale_non_uniform(w as f64 / region.width().max(1e-9), h as f64 / region.height().max(1e-9))
* Affine::translate(-region.origin().to_vec2());
let opts = RenderOptions::default();
let opts = RenderOptions { mark_undecoded_images, ..Default::default() };
r.image_left_out = Some(false);
let pattern_images_left_out = images_left_out();
r.check_plugins();
@@ -254,7 +262,7 @@ pub(crate) fn set_pattern_paint(ctx: &mut RenderContext, name: &str, xf: Affine,
let (pw, ph) = def.period();
let scale = raster_scale(s, (pw, ph));
let params = (def.tile, def.tile_type, def.overlap);
let key = (name.to_string(), scale.to_bits(), ink);
let key = (name.to_string(), scale.to_bits(), ink, f.opts.mark_undecoded_images);
let cms = vectorcraft_color::cms::active();
let hit = CACHE.with(|c| {
let mut c = c.borrow_mut();
@@ -283,7 +291,9 @@ pub(crate) fn set_pattern_paint(ctx: &mut RenderContext, name: &str, xf: Affine,
let w = (pw * scale).ceil().clamp(1.0, 4096.0) as u16;
let h = (ph * scale).ceil().clamp(1.0, 4096.0) as u16;
let before = crate::paint::decode_failures();
let Some((pm, image_left_out)) = with_tile_renderer(|r| rasterize(r, doc, def, Rect::new(0.0, 0.0, pw, ph), w, h, ink)) else {
let Some((pm, image_left_out)) =
with_tile_renderer(|r| rasterize(r, doc, def, Rect::new(0.0, 0.0, pw, ph), (w, h), ink, f.opts.mark_undecoded_images))
else {
ctx.set_paint(missing());
return true;
};
@@ -322,7 +332,7 @@ pub fn render_pattern_swatch(doc: &Document, name: &str, size: u32) -> Option<Re
let side = w.max(h);
let region = Rect::from_center_size((w / 2.0, h / 2.0), (side, side));
let px = size.clamp(1, 1024) as u16;
let (pm, _) = with_tile_renderer(|r| rasterize(r, doc, def, region, px, px, Ink::Display))?;
let (pm, _) = with_tile_renderer(|r| rasterize(r, doc, def, region, (px, px), Ink::Display, false))?;
Some(Rendered { width: px as u32, height: px as u32, pixels: pm.data_as_u8_slice().to_vec() })
}
-2
View File
@@ -207,8 +207,6 @@ impl Renderer {
let failures = crate::paint::decode_failures();
let pattern_images_left_out = crate::pattern::images_left_out();
self.draw_kept_node(&mut mctx, &Frame { mt: false, ink: Ink::Display, ..*f }, &m.art);
// Pattern tiles are drawn without undecoded-image marks, so an image a tile left out counts
// with the marks on too.
let incomplete = self.image_left_out.unwrap_or(false)
|| crate::pattern::images_left_out() > pattern_images_left_out
|| (!f.opts.mark_undecoded_images && crate::paint::decode_failures() > failures);
+65 -3
View File
@@ -944,9 +944,33 @@ fn opacity_mask_with_missing_image_in_pattern_leaves_out_object() {
let img = r.render(&d, 100, 100, Affine::IDENTITY, &opts);
assert_eq!(img.pixel(70, 50), [255, 255, 255, 255], "frame {frame}: incomplete pattern mask leaves out the object");
}
// Pattern tiles draw no undecoded-image mark, so the object stays out with the marks on too.
let marked = Renderer::new().render(&d, 100, 100, Affine::IDENTITY, &RenderOptions { mark_undecoded_images: true, ..opts.clone() });
assert_eq!(marked.pixel(70, 50), [255, 255, 255, 255], "a tile without the mark leaves out the object");
}
#[test]
fn opacity_mask_with_marked_image_in_pattern_draws_object() {
use vectorcraft_doc::pattern::{PatternDef, pattern_paint};
use vectorcraft_doc::{ImageBlob, ImageObject, NodeKind, OpacityMask};
let mut d = Document::new(100.0, 100.0);
d.images.insert("cut-mask".into(), ImageBlob::new("image/png", b"not a PNG: pattern mask".to_vec()));
let image =
ImageObject { key: "cut-mask".into(), width: 20, height: 20, xf: Affine::IDENTITY, link: None, placement: Default::default(), pixels: None };
let image = Node::new(d.alloc_id(), NodeKind::Image(Box::new(image)));
let mut pattern = PatternDef::new("Cut mask", vec![Arc::new(image)]);
pattern.tile = Rect::new(0.0, 0.0, 20.0, 20.0);
d.patterns.push(pattern);
let art = rect_node(&mut d, Rect::new(10.0, 10.0, 50.0, 90.0), pattern_paint("Cut mask"), Paint::None, 0.0);
let mut object = 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);
object.extras_mut().mask = Some(Box::new(OpacityMask::new(art, false)));
d.insert(Some(d.layers[0].id), 0, object).unwrap();
let mut r = Renderer::new();
let opts = RenderOptions { background: Some([255, 255, 255, 255]), ..Default::default() };
let off = r.render(&d, 100, 100, Affine::IDENTITY, &opts);
assert_eq!(off.pixel(70, 50), [255, 255, 255, 255], "an unmarked missing image leaves out the object");
let on = r.render(&d, 100, 100, Affine::IDENTITY, &RenderOptions { mark_undecoded_images: true, ..opts });
assert_eq!(on.pixel(70, 50), [255, 0, 0, 255], "the marked image keeps the mask complete");
assert!(on.pixel(15, 15)[1] < 255, "the marked pattern contributes mask coverage");
}
#[test]
@@ -1488,6 +1512,44 @@ fn an_undecodable_image_is_tried_once_and_marked_on_screen() {
assert_eq!((attempts() - tried, decode_failures() - failed), (1, 3), "and counts it without decoding it again");
}
#[test]
fn pattern_tile_follows_undecoded_image_mark_in_both_cache_orders() {
use vectorcraft_doc::pattern::{PatternDef, pattern_paint};
use vectorcraft_doc::{ImageBlob, ImageObject, NodeKind};
let mut d = Document::new(80.0, 80.0);
d.images.insert("cut-pattern".into(), ImageBlob::new("image/png", b"not a PNG: pattern fill".to_vec()));
let image = ImageObject {
key: "cut-pattern".into(),
width: 20,
height: 20,
xf: Affine::IDENTITY,
link: None,
placement: Default::default(),
pixels: None,
};
let image = Node::new(d.alloc_id(), NodeKind::Image(Box::new(image)));
let mut pattern = PatternDef::new("Cut pattern", vec![Arc::new(image)]);
pattern.tile = Rect::new(0.0, 0.0, 20.0, 20.0);
d.patterns.push(pattern);
let shape = rect_node(&mut d, Rect::new(10.0, 10.0, 70.0, 70.0), pattern_paint("Cut pattern"), Paint::None, 0.0);
d.insert(Some(d.layers[0].id), 0, shape).unwrap();
let mut r = Renderer::new();
for order in [[false, true, false], [true, false, true]] {
for mark in order {
let opts = RenderOptions { mark_undecoded_images: mark, ..Default::default() };
let pixel = r.render(&d, 80, 80, Affine::IDENTITY, &opts).pixel(15, 15);
assert_eq!(pixel[3] > 0, mark, "mark_undecoded_images {mark}: {pixel:?}");
if mark {
assert_eq!(pixel[0], pixel[1], "the pattern draws a gray mark");
assert_eq!(pixel[1], pixel[2], "the pattern draws a gray mark");
assert!((120..=140).contains(&pixel[0]), "the pattern draws a gray mark: {pixel:?}");
}
}
}
}
/// The rasters a renderer keeps of an object's raster effects are made again when
/// `mark_undecoded_images` changes: a blurred image whose pixels can't be decoded shows its blurred
/// mark only while the option is on.