use darkly::coord::{CanvasPoint, CanvasRect};
use darkly::document::SelectionMode;
use darkly::engine::types::StrokeOp;
use darkly::engine::DarklyEngine;
use darkly::gpu::context::GpuContext;
use darkly::gpu::test_utils::*;
use darkly::layer::LayerId;
fn test_engine(width: u32, height: u32) -> DarklyEngine {
let (device, queue) = test_device();
let gpu = GpuContext::new_headless(device, queue);
DarklyEngine::new(gpu, width, height)
}
fn distinct_rgba(w: u32, h: u32) -> Vec<u8> {
let mut v = vec![0u8; (w * h * 4) as usize];
for y in 0..h {
for x in 0..w {
let i = ((y * w + x) * 4) as usize;
v[i] = (x * 10 + 3) as u8;
v[i + 1] = (y * 10 + 7) as u8;
v[i + 2] = ((x + y) * 5 + 1) as u8;
v[i + 3] = 255;
}
}
v
}
fn px(buf: &[u8], stride: u32, x: u32, y: u32) -> [u8; 4] {
let i = ((y * stride + x) * 4) as usize;
[buf[i], buf[i + 1], buf[i + 2], buf[i + 3]]
}
fn inv(p: [u8; 4]) -> [u8; 4] {
[255 - p[0], 255 - p[1], 255 - p[2], p[3]]
}
fn paint_dab(engine: &mut DarklyEngine, node_id: LayerId, x: f32, y: f32, value: f32) {
engine.begin_stroke(node_id);
engine.stroke_to(StrokeOp::BrushStroke {
x,
y,
pressure: 1.0,
x_tilt: 0.0,
y_tilt: 0.0,
rotation: 0.0,
tangential_pressure: 0.0,
time_ms: 0.0,
cr: value,
cg: value,
cb: value,
ca: 1.0,
});
engine.end_stroke();
engine.render(0.0);
}
#[test]
fn invert_layer_negates_every_channel() {
let (w, h) = (7u32, 5u32);
let mut e = test_engine(w, h);
let layer = e.paste_image(w, h, &distinct_rgba(w, h), 0, 0, None);
let before = e.test_readback_layer(layer);
assert!(e.apply_filter(layer, "invert"));
let after = e.test_readback_layer(layer);
for y in 0..h {
for x in 0..w {
assert_eq!(
px(&after, w, x, y),
inv(px(&before, w, x, y)),
"invert: ({x},{y}) should be 255-c with alpha preserved"
);
}
}
}
#[test]
fn invert_layer_twice_is_identity() {
let (w, h) = (7u32, 5u32);
let mut e = test_engine(w, h);
let layer = e.paste_image(w, h, &distinct_rgba(w, h), 0, 0, None);
let before = e.test_readback_layer(layer);
assert!(e.apply_filter(layer, "invert"));
assert!(e.apply_filter(layer, "invert"));
assert_eq!(
e.test_readback_layer(layer),
before,
"invert twice restores the original pixels exactly"
);
}
#[test]
fn invert_layer_undo_redo_round_trips() {
let (w, h) = (7u32, 5u32);
let mut e = test_engine(w, h);
let layer = e.paste_image(w, h, &distinct_rgba(w, h), 0, 0, None);
let before = e.test_readback_layer(layer);
assert!(e.apply_filter(layer, "invert"));
let inverted = e.test_readback_layer(layer);
e.undo();
assert_eq!(e.test_readback_layer(layer), before, "undo restores pixels");
e.redo();
assert_eq!(
e.test_readback_layer(layer),
inverted,
"redo re-applies the invert"
);
}
#[test]
fn invert_unknown_type_is_a_noop() {
let (w, h) = (4u32, 4u32);
let mut e = test_engine(w, h);
let layer = e.paste_image(w, h, &distinct_rgba(w, h), 0, 0, None);
let before = e.test_readback_layer(layer);
assert!(
!e.apply_filter(layer, "no_such_adjustment"),
"an unregistered type must return false"
);
assert_eq!(
e.test_readback_layer(layer),
before,
"an unknown filter must not touch pixels"
);
}
#[test]
fn invert_layer_with_rect_selection_only_inverts_selection() {
let (w, h) = (12u32, 12u32);
let mut e = test_engine(w, h);
let layer = e.paste_image(w, h, &distinct_rgba(w, h), 0, 0, None);
let before = e.test_readback_layer(layer);
e.select_rect(3.0, 3.0, 5.0, 5.0, SelectionMode::Replace, false, 0.0);
assert!(e.apply_filter(layer, "invert"));
let after = e.test_readback_layer(layer);
assert_eq!(px(&after, w, 4, 4), inv(px(&before, w, 4, 4)));
assert_eq!(px(&after, w, 7, 7), inv(px(&before, w, 7, 7)));
assert_eq!(px(&after, w, 0, 0), px(&before, w, 0, 0));
assert_eq!(px(&after, w, 10, 10), px(&before, w, 10, 10));
e.undo();
assert_eq!(
e.test_readback_layer(layer),
before,
"undo restores the layer"
);
}
#[test]
fn invert_layer_with_ellipse_selection_clips_to_shape() {
let (w, h) = (12u32, 12u32);
let mut e = test_engine(w, h);
let layer = e.paste_image(w, h, &distinct_rgba(w, h), 0, 0, None);
let before = e.test_readback_layer(layer);
e.select_ellipse(2.0, 2.0, 8.0, 8.0, SelectionMode::Replace, false, 0.0);
assert!(e.apply_filter(layer, "invert"));
let after = e.test_readback_layer(layer);
assert_eq!(
px(&after, w, 2, 2),
px(&before, w, 2, 2),
"bbox corner outside the ellipse must be untouched"
);
assert_eq!(px(&after, w, 6, 6), inv(px(&before, w, 6, 6)));
e.undo();
assert_eq!(e.test_readback_layer(layer), before);
}
#[test]
fn invert_mask_negates_r8_and_round_trips() {
let (w, h) = (16u32, 16u32);
let mut e = test_engine(w, h);
let layer = e.add_raster_layer(None);
e.add_mask(layer);
let mask = e.test_mask_id(layer).expect("mask present");
paint_dab(&mut e, mask, (w / 2) as f32, (h / 2) as f32, 0.0);
let before = e.test_readback_layer(mask);
assert_eq!(
before.len(),
(w * h) as usize,
"mask is R8 — one byte/pixel"
);
assert!(e.apply_filter(mask, "invert"));
let after = e.test_readback_layer(mask);
for i in 0..before.len() {
assert_eq!(after[i], 255 - before[i], "mask byte {i} should be 1-r");
}
assert!(e.apply_filter(mask, "invert"));
assert_eq!(
e.test_readback_layer(mask),
before,
"invert twice restores the mask exactly"
);
}
#[test]
fn invert_mask_with_selection_only_inverts_selected_region() {
let (w, h) = (12u32, 12u32);
let mut e = test_engine(w, h);
let layer = e.add_raster_layer(None);
e.add_mask(layer);
let mask = e.test_mask_id(layer).expect("mask present");
let before = e.test_readback_layer(mask);
e.select_rect(3.0, 3.0, 5.0, 5.0, SelectionMode::Replace, false, 0.0);
assert!(e.apply_filter(mask, "invert"));
let after = e.test_readback_layer(mask);
let at = |buf: &[u8], x: u32, y: u32| buf[(y * w + x) as usize];
assert_eq!(at(&after, 4, 4), 255 - at(&before, 4, 4));
assert_eq!(at(&after, 7, 7), 255 - at(&before, 7, 7));
assert_eq!(at(&after, 0, 0), at(&before, 0, 0));
assert_eq!(at(&after, 10, 10), at(&before, 10, 10));
e.undo();
assert_eq!(
e.test_readback_layer(mask),
before,
"undo restores the mask"
);
}
#[test]
fn invert_layer_with_selection_after_crop_uses_plane_coords() {
let (w, h) = (32u32, 32u32);
let mut e = test_engine(w, h);
let layer = e.paste_image(w, h, &distinct_rgba(w, h), 0, 0, None);
let before = e.test_readback_layer(layer);
e.resize_canvas(CanvasRect::new(CanvasPoint::new(8, 8), 16, 16));
e.select_rect(10.0, 10.0, 8.0, 8.0, SelectionMode::Replace, false, 0.0);
assert!(e.apply_filter(layer, "invert"));
let after = e.test_readback_layer(layer);
assert_eq!(px(&after, w, 12, 12), inv(px(&before, w, 12, 12)));
assert_eq!(px(&after, w, 17, 17), inv(px(&before, w, 17, 17)));
assert_eq!(px(&after, w, 2, 2), px(&before, w, 2, 2));
assert_eq!(px(&after, w, 25, 25), px(&before, w, 25, 25));
assert_eq!(px(&after, w, 18, 18), px(&before, w, 18, 18));
e.undo();
assert_eq!(
e.test_readback_layer(layer),
before,
"undo restores the layer"
);
}
#[test]
fn invert_layer_with_selection_after_rescale() {
let (w, h) = (16u32, 16u32);
let mut e = test_engine(w, h);
let layer = e.paste_image(w, h, &distinct_rgba(w, h), 0, 0, None);
e.rescale_image(2 * w, 2 * h);
let nw = 2 * w;
let before = e.test_readback_layer(layer);
assert_eq!(
before.len(),
(nw * 2 * h * 4) as usize,
"layer must be resampled to the doubled dims"
);
e.select_rect(8.0, 8.0, 16.0, 16.0, SelectionMode::Replace, false, 0.0);
assert!(e.apply_filter(layer, "invert"));
let after = e.test_readback_layer(layer);
assert_eq!(px(&after, nw, 12, 12), inv(px(&before, nw, 12, 12)));
assert_eq!(px(&after, nw, 20, 20), inv(px(&before, nw, 20, 20)));
assert_eq!(px(&after, nw, 2, 2), px(&before, nw, 2, 2));
assert_eq!(px(&after, nw, 30, 30), px(&before, nw, 30, 30));
e.undo();
assert_eq!(
e.test_readback_layer(layer),
before,
"undo restores the rescaled layer"
);
}
#[test]
fn invert_mask_with_selection_after_crop() {
let (w, h) = (32u32, 32u32);
let mut e = test_engine(w, h);
let layer = e.add_raster_layer(None);
e.add_mask(layer);
let mask = e.test_mask_id(layer).expect("mask present");
paint_dab(&mut e, mask, 14.0, 14.0, 0.0);
e.resize_canvas(CanvasRect::new(CanvasPoint::new(8, 8), 16, 16));
let before = e.test_readback_layer(mask);
assert_eq!(
before.len(),
(w * h) as usize,
"mask keeps its full extent across the crop (R8, one byte/pixel)"
);
e.select_rect(10.0, 10.0, 8.0, 8.0, SelectionMode::Replace, false, 0.0);
assert!(e.apply_filter(mask, "invert"));
let after = e.test_readback_layer(mask);
let at = |buf: &[u8], x: u32, y: u32| buf[(y * w + x) as usize];
assert_eq!(at(&after, 12, 12), 255 - at(&before, 12, 12));
assert_eq!(at(&after, 17, 17), 255 - at(&before, 17, 17));
assert_eq!(at(&after, 2, 2), at(&before, 2, 2));
assert_eq!(at(&after, 18, 18), at(&before, 18, 18));
e.undo();
assert_eq!(
e.test_readback_layer(mask),
before,
"undo restores the mask"
);
}
fn fill_layer(engine: &mut DarklyEngine, layer_id: LayerId, r: u8, g: u8, b: u8) {
engine.begin_stroke(layer_id);
engine.stroke_to(StrokeOp::FloodFill {
x: 1.0,
y: 1.0,
r,
g,
b,
a: 255,
tolerance: 0,
});
engine.end_stroke();
engine.render(0.0);
}
#[test]
fn filter_layer_at_root_inverts_everything_below() {
let (cw, ch) = (16u32, 16u32);
let mut engine = test_engine(cw, ch);
let red = engine.add_raster_layer(None);
fill_layer(&mut engine, red, 255, 0, 0);
let _filter = engine
.add_filter_layer("invert", vec![], None)
.expect("invert is a registered filter type");
engine.test_flush_readbacks();
engine.render(0.0);
let canvas = engine.test_readback_canvas();
let p = px(&canvas, cw, cw / 2, ch / 2);
assert_eq!(
p,
[0, 255, 255, 255],
"invert filter layer must turn the red layer below it cyan; got {p:?}"
);
}
#[test]
fn filter_layer_does_not_affect_layers_above_it() {
let (cw, ch) = (16u32, 16u32);
let mut engine = test_engine(cw, ch);
let red = engine.add_raster_layer(None);
fill_layer(&mut engine, red, 255, 0, 0);
let _filter = engine.add_filter_layer("invert", vec![], None).unwrap();
let blue = engine.add_raster_layer(None);
fill_layer(&mut engine, blue, 0, 0, 255);
engine.test_flush_readbacks();
engine.render(0.0);
let canvas = engine.test_readback_canvas();
let p = px(&canvas, cw, cw / 2, ch / 2);
assert_eq!(
p,
[0, 0, 255, 255],
"the opaque blue layer above the filter must be unaffected; got {p:?}"
);
}
#[test]
fn filter_layer_in_isolated_group_is_scoped() {
let (cw, ch) = (16u32, 16u32);
let mut engine = test_engine(cw, ch);
let outside = engine.add_raster_layer(None);
fill_layer(&mut engine, outside, 255, 0, 0);
let group = engine.add_group(None);
engine.set_group_passthrough(group, false);
let inside = engine.add_raster_layer(Some(group));
fill_layer(&mut engine, inside, 255, 0, 0);
let _filter = engine
.add_filter_layer("invert", vec![], Some(group))
.unwrap();
engine.test_flush_readbacks();
engine.render(0.0);
let with_content = engine.test_readback_canvas();
let p = px(&with_content, cw, cw / 2, ch / 2);
assert_eq!(
p,
[0, 255, 255, 255],
"filter must invert its isolated group's lower sibling; got {p:?}"
);
engine.set_layer_visible(inside, false);
engine.render(0.0);
let leak_check = engine.test_readback_canvas();
let p = px(&leak_check, cw, cw / 2, ch / 2);
assert_eq!(
p,
[255, 0, 0, 255],
"an isolated group's filter must not affect layers below the group; got {p:?}"
);
}
#[test]
fn masked_filter_layer_confines_inversion() {
let (cw, ch) = (16u32, 16u32);
let mut engine = test_engine(cw, ch);
let red = engine.add_raster_layer(None);
fill_layer(&mut engine, red, 255, 0, 0);
let filter = engine.add_filter_layer("invert", vec![], None).unwrap();
engine.select_rect(
0.0,
0.0,
(cw / 2) as f32,
ch as f32,
SelectionMode::Replace,
false,
0.0,
);
engine.add_mask(filter);
let mask = engine.test_mask_id(filter).expect("mask present on filter");
engine.clear_selection();
engine.test_flush_readbacks();
engine.render(0.0);
let canvas = engine.test_readback_canvas();
assert_eq!(
px(&canvas, cw, cw / 4, ch / 2),
[0, 255, 255, 255],
"masked-in half must invert to cyan",
);
assert_eq!(
px(&canvas, cw, 3 * cw / 4, ch / 2),
[255, 0, 0, 255],
"masked-out half must keep the original red",
);
paint_dab(&mut engine, mask, (3 * cw / 4) as f32, (ch / 2) as f32, 0.5);
engine.test_flush_readbacks();
engine.render(0.0);
let canvas = engine.test_readback_canvas();
let p = px(&canvas, cw, 3 * cw / 4, ch / 2);
assert!(
p[0] > 40 && p[0] < 215 && p[1] > 40 && p[1] < 215,
"a mid-gray mask value must lerp red↔cyan (got {p:?}), proving soft masking",
);
}
#[test]
fn masked_filter_layer_in_isolated_group_lerps_against_group_accum() {
let (cw, ch) = (16u32, 16u32);
let mut engine = test_engine(cw, ch);
let outside = engine.add_raster_layer(None);
fill_layer(&mut engine, outside, 0, 255, 0);
let group = engine.add_group(None);
engine.set_group_passthrough(group, false);
let inside = engine.add_raster_layer(Some(group));
fill_layer(&mut engine, inside, 255, 0, 0);
let filter = engine
.add_filter_layer("invert", vec![], Some(group))
.unwrap();
engine.select_rect(
0.0,
0.0,
(cw / 2) as f32,
ch as f32,
SelectionMode::Replace,
false,
0.0,
);
engine.add_mask(filter);
engine.clear_selection();
engine.test_flush_readbacks();
engine.render(0.0);
let canvas = engine.test_readback_canvas();
assert_eq!(
px(&canvas, cw, cw / 4, ch / 2),
[0, 255, 255, 255],
"masked-in half inverts the group's own content to cyan",
);
assert_eq!(
px(&canvas, cw, 3 * cw / 4, ch / 2),
[255, 0, 0, 255],
"masked-out half keeps the group's red, with no canvas leak",
);
}
#[test]
fn filter_layer_is_non_destructive() {
let (cw, ch) = (16u32, 16u32);
let mut engine = test_engine(cw, ch);
let red = engine.add_raster_layer(None);
fill_layer(&mut engine, red, 255, 0, 0);
engine.test_flush_readbacks();
engine.render(0.0);
let original = engine.test_readback_canvas();
assert_eq!(px(&original, cw, cw / 2, ch / 2), [255, 0, 0, 255]);
let filter = engine.add_filter_layer("invert", vec![], None).unwrap();
engine.render(0.0);
assert_eq!(
px(&engine.test_readback_canvas(), cw, cw / 2, ch / 2),
[0, 255, 255, 255],
"filter visible → cyan"
);
engine.set_layer_visible(filter, false);
engine.render(0.0);
assert_eq!(
engine.test_readback_canvas(),
original,
"hiding the filter must restore the original composite exactly"
);
engine.set_layer_visible(filter, true);
engine.render(0.0);
assert_eq!(
px(&engine.test_readback_canvas(), cw, cw / 2, ch / 2),
[0, 255, 255, 255],
"re-showing the filter inverts again"
);
engine.remove_layer(filter).expect("filter layer removable");
engine.render(0.0);
assert_eq!(
engine.test_readback_canvas(),
original,
"deleting the filter must restore the original composite exactly"
);
}