Merge pull request #1541 from switch87/fix/1536-effect-raster-paint-transform

Render: effect rasters drawn after a pattern fill keep their place and size
This commit is contained in:
BFlat 2026-10-13 09:07:48 +07:00 committed by GitHub
commit 632ea2a9f3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 56 additions and 0 deletions

View File

@ -274,6 +274,9 @@ fn draw_scaled(ctx: &mut RenderContext, image: std::sync::Arc<vello_cpu::Pixmap>
// On whole pixels at full size the cheapest sampler is exact.
let quality = if k < 1.0 { peniko::ImageQuality::Medium } else { peniko::ImageQuality::Low };
let sampler = peniko::ImageSampler { quality, ..Default::default() };
// The raster maps 1:1 onto the rect: a paint transform left by an earlier fill (a pattern,
// radial or freeform gradient) would resample it (#1536).
ctx.reset_paint_transform();
ctx.set_paint(vello_cpu::Image { image: vello_cpu::ImageSource::Pixmap(image), sampler });
ctx.fill_rect(&Rect::new(0.0, 0.0, w, h));
ctx.set_transform(Affine::IDENTITY);
@ -617,6 +620,7 @@ impl Renderer {
ctx.set_transform(Affine::translate((x, y)));
ctx.set_blend_mode(blend_mode(mode));
let sampler = peniko::ImageSampler { quality: peniko::ImageQuality::Low, ..Default::default() };
ctx.reset_paint_transform();
ctx.set_paint(vello_cpu::Image { image: vello_cpu::ImageSource::Pixmap(image), sampler });
ctx.fill_rect(&Rect::new(0.0, 0.0, w, h));
ctx.set_blend_mode(blend_mode(vectorcraft_doc::color::BlendMode::Normal));

View File

@ -274,6 +274,58 @@ mod tests {
p[0] > 200 && p[1] > 200 && p[2] > 200
}
/// #1536: a raster effect drawn after a pattern fill is the same as without the pattern (the
/// pattern's paint transform must not resample the effect's raster).
#[test]
fn effects_after_a_pattern_fill_keep_their_place_and_size() {
let fx = |id: &str, params: serde_json::Value| vectorcraft_doc::Effect { id: id.into(), params, visible: true };
for effect in [
fx("blur.gaussian", serde_json::json!({"radius": 4.0})),
fx("stylize.dropShadow", serde_json::json!({"x": 6, "y": 6, "blur": 3, "opacity": 100})),
] {
let make = |with_pattern: bool| {
let mut d = doc_with_pattern(TileType::Grid);
let l = d.layers[0].id;
let filled = d.children(Some(l)).unwrap()[0].id;
d.remove(filled).unwrap();
if with_pattern {
let id = d.alloc_id();
let n = Node::path(
id,
shapes::rectangle(Rect::new(0.0, 0.0, 80.0, 80.0)),
Appearance::basic(pattern_paint("Checks"), Paint::None, 0.0),
);
d.insert(Some(l), 0, n).unwrap();
}
let id = d.alloc_id();
let mut n = Node::path(
id,
shapes::ellipse(Rect::new(120.0, 110.0, 180.0, 170.0)),
Appearance::basic(Paint::solid(Color::rgb(0.5, 0.5, 0.5)), Paint::None, 0.0),
);
n.appearance.effects.push(effect.clone());
let at = d.children(Some(l)).map_or(0, Vec::len);
d.insert(Some(l), at, n).unwrap();
d
};
// Supersampled, as raster exports draw.
let one = |d: &Document| {
let mut r = Renderer::new();
r.render(
d,
200,
200,
Affine::IDENTITY,
&RenderOptions { background: Some([255, 255, 255, 255]), supersample: true, ..Default::default() },
)
};
let (with, without) = (one(&make(true)), one(&make(false)));
for (x, y) in [(150, 140), (125, 140), (176, 160), (150, 172), (184, 176)] {
assert_eq!(with.pixel(x, y), without.pixel(x, y), "{} at ({x}, {y})", effect.id);
}
}
}
#[test]
fn pattern_fill_is_periodic() {
let d = doc_with_pattern(TileType::Grid);