use std::sync::Arc;
use agg_gui::color::Color;
use agg_gui::draw_ctx::DrawCtx;
use crate::layer_text_readback_tests::{px, try_device, Target};
use crate::WgpuGfxCtx;
const SIZE: u32 = 64;
fn red() -> Color {
Color::rgba(1.0, 0.0, 0.0, 1.0)
}
fn blue() -> Color {
Color::rgba(0.0, 0.0, 1.0, 1.0)
}
fn green() -> Color {
Color::rgba(0.0, 1.0, 0.0, 1.0)
}
fn is_blue(p: [u8; 4]) -> bool {
p[2] > 200 && p[0] < 60 && p[1] < 60
}
fn is_red(p: [u8; 4]) -> bool {
p[0] > 200 && p[1] < 60 && p[2] < 60
}
fn at(data: &[u8], x: u32, y_up: u32) -> [u8; 4] {
px(data, SIZE, x, SIZE - 1 - y_up)
}
fn fill_all(ctx: &mut WgpuGfxCtx, color: Color) {
ctx.set_fill_color(color);
ctx.begin_path();
ctx.rect(-200.0, -200.0, 600.0, 600.0);
ctx.fill();
}
fn render(paint: impl FnOnce(&mut WgpuGfxCtx)) -> Option<Vec<u8>> {
let (device, queue) = try_device()?;
let target = Target::new(Arc::clone(&device), Arc::clone(&queue), SIZE, SIZE);
let mut ctx = WgpuGfxCtx::new(
device,
queue,
wgpu::TextureFormat::Rgba8Unorm,
SIZE as f32,
SIZE as f32,
);
ctx.reset(SIZE as f32, SIZE as f32);
assert!(ctx.supports_clip_path());
ctx.clear(red());
paint(&mut ctx);
ctx.flush_to_surface(&target.view);
Some(target.read())
}
#[test]
fn clip_path_masks_fill_to_the_circle() {
let Some(data) = render(|ctx| {
ctx.save();
ctx.begin_path();
ctx.circle(32.0, 32.0, 16.0);
ctx.clip_path();
fill_all(ctx, blue());
ctx.restore();
}) else {
eprintln!("SKIP: no GPU adapter");
return;
};
assert!(is_blue(at(&data, 32, 32)), "centre must be blue");
assert!(is_red(at(&data, 1, 1)), "corner must stay red");
assert!(
is_red(at(&data, 52, 32)),
"just past the circle edge must be red; got {:?}",
at(&data, 52, 32)
);
}
#[test]
fn clip_is_released_after_restore() {
let Some(data) = render(|ctx| {
ctx.save();
ctx.begin_path();
ctx.circle(32.0, 32.0, 16.0);
ctx.clip_path();
fill_all(ctx, blue());
ctx.restore();
fill_all(ctx, green());
}) else {
eprintln!("SKIP: no GPU adapter");
return;
};
let corner = at(&data, 1, 1);
assert!(
corner[1] > 200 && corner[0] < 60,
"corner must be green after restore; got {corner:?}"
);
}
#[test]
fn clip_path_respects_translate_and_rotate() {
let Some(data) = render(|ctx| {
ctx.save();
ctx.translate(32.0, 32.0);
ctx.rotate(std::f64::consts::FRAC_PI_4);
ctx.begin_path();
ctx.rect(-10.0, -10.0, 20.0, 20.0);
ctx.clip_path();
fill_all(ctx, blue());
ctx.restore();
}) else {
eprintln!("SKIP: no GPU adapter");
return;
};
assert!(is_blue(at(&data, 32, 32)), "centre must be blue");
assert!(
is_blue(at(&data, 32, 42)),
"on-axis point inside the diamond must be blue; got {:?}",
at(&data, 32, 42)
);
assert!(
is_red(at(&data, 44, 44)),
"the unrotated square's corner direction must stay red; got {:?}",
at(&data, 44, 44)
);
}
#[test]
fn nested_clip_paths_intersect() {
let Some(data) = render(|ctx| {
ctx.save();
ctx.begin_path();
ctx.rect(0.0, 0.0, 32.0, 64.0); ctx.clip_path();
ctx.save();
ctx.begin_path();
ctx.rect(0.0, 0.0, 64.0, 32.0); ctx.clip_path();
fill_all(ctx, blue());
ctx.restore();
ctx.restore();
}) else {
eprintln!("SKIP: no GPU adapter");
return;
};
assert!(is_blue(at(&data, 16, 16)), "bottom-left must be blue");
assert!(is_red(at(&data, 48, 16)), "bottom-right must be red");
assert!(is_red(at(&data, 16, 48)), "top-left must be red");
assert!(is_red(at(&data, 48, 48)), "top-right must be red");
}
#[test]
fn empty_path_clip_draws_nothing() {
let Some(data) = render(|ctx| {
ctx.save();
ctx.begin_path();
ctx.clip_path();
fill_all(ctx, blue());
ctx.restore();
}) else {
eprintln!("SKIP: no GPU adapter");
return;
};
assert!(
is_red(at(&data, 32, 32)),
"an empty clip must draw nothing; got {:?}",
at(&data, 32, 32)
);
}
fn is_green(p: [u8; 4]) -> bool {
p[1] > 200 && p[0] < 60 && p[2] < 60
}
#[test]
fn clip_path_keeps_the_current_path_for_a_later_stroke() {
let Some(data) = render(|ctx| {
ctx.set_fill_color(red());
ctx.begin_path();
ctx.circle(32.0, 32.0, 16.0);
ctx.fill();
ctx.save();
ctx.clip_path();
fill_all(ctx, blue());
ctx.restore();
ctx.set_stroke_color(green());
ctx.set_line_width(6.0);
ctx.stroke();
}) else {
eprintln!("SKIP: no GPU adapter");
return;
};
assert!(
is_green(at(&data, 48, 32)),
"stroke must follow the clipping circle; got {:?}",
at(&data, 48, 32)
);
assert!(
is_blue(at(&data, 32, 32)),
"centre must stay blue; got {:?}",
at(&data, 32, 32)
);
}
#[test]
fn balanced_save_restore_inside_clip_does_not_pop_it() {
let Some(data) = render(|ctx| {
ctx.save();
ctx.begin_path();
ctx.circle(32.0, 32.0, 16.0);
ctx.clip_path();
ctx.save();
ctx.set_fill_color(green());
ctx.restore();
fill_all(ctx, blue());
ctx.restore();
}) else {
eprintln!("SKIP: no GPU adapter");
return;
};
assert!(is_blue(at(&data, 32, 32)), "centre must be blue");
assert!(
is_red(at(&data, 1, 1)),
"the clip must still be active after the inner restore; got {:?}",
at(&data, 1, 1)
);
}
#[test]
fn clip_path_inside_a_compositing_layer() {
let Some(data) = render(|ctx| {
ctx.push_layer(SIZE as f64, SIZE as f64);
ctx.save();
ctx.begin_path();
ctx.circle(32.0, 32.0, 16.0);
ctx.clip_path();
fill_all(ctx, blue());
ctx.restore();
ctx.pop_layer();
}) else {
eprintln!("SKIP: no GPU adapter");
return;
};
assert!(is_blue(at(&data, 32, 32)), "centre must be blue");
assert!(is_red(at(&data, 1, 1)), "corner must stay red");
}
#[test]
fn compositing_layer_inside_a_clip_path() {
let Some(data) = render(|ctx| {
ctx.save();
ctx.begin_path();
ctx.circle(32.0, 32.0, 16.0);
ctx.clip_path();
ctx.push_layer(SIZE as f64, SIZE as f64);
fill_all(ctx, blue());
ctx.pop_layer();
ctx.restore();
}) else {
eprintln!("SKIP: no GPU adapter");
return;
};
assert!(is_blue(at(&data, 32, 32)), "centre must be blue");
assert!(is_red(at(&data, 1, 1)), "corner must stay red");
}
#[test]
fn clip_path_edge_is_anti_aliased() {
let Some(data) = render(|ctx| {
ctx.save();
ctx.begin_path();
ctx.circle(32.0, 32.0, 16.0);
ctx.clip_path();
fill_all(ctx, blue());
ctx.restore();
}) else {
eprintln!("SKIP: no GPU adapter");
return;
};
let row: Vec<[u8; 4]> = (44..54).map(|x| at(&data, x, 32)).collect();
assert!(
row.iter()
.any(|p| p[0] > 0 && p[0] < 255 && p[2] > 0 && p[2] < 255),
"no anti-aliased pixel on the clip edge: {row:?}"
);
}