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::overlay::{OverlayPrimitive, FLAG_CANVAS_SPACE, KIND_DASHED_LINE};
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 alpha_at(pixels: &[u8], w: u32, x: u32, y: u32) -> u8 {
pixels[((y * w + x) * 4 + 3) as usize]
}
fn paint_row(engine: &mut DarklyEngine, layer_id: LayerId, py: f32, x0: f32, x1: f32) {
engine.begin_stroke(layer_id);
let steps = 48;
for i in 0..=steps {
let x = x0 + (x1 - x0) * (i as f32 / steps as f32);
engine.stroke_to(StrokeOp::BrushStroke {
x,
y: py,
pressure: 1.0,
x_tilt: 0.0,
y_tilt: 0.0,
rotation: 0.0,
tangential_pressure: 0.0,
time_ms: i as f64 * 16.0,
cr: 1.0,
cg: 0.0,
cb: 0.0,
ca: 1.0,
});
}
engine.end_stroke();
}
#[test]
fn marquee_selection_masks_same_plane_pixels_after_crop() {
let (w, h) = (64u32, 64u32);
let mut engine = test_engine(w, h);
let layer_id = engine.add_raster_layer(None);
engine.select_rect(8.0, 0.0, 24.0, h as f32, SelectionMode::Replace, false, 0.0);
engine.resize_canvas(CanvasRect::from_xywh(8, 0, 40, h));
assert_eq!(engine.canvas_rect().origin, CanvasPoint::new(8, 0));
paint_row(&mut engine, layer_id, 32.0, 8.0, 48.0);
let px = engine.test_readback_layer(layer_id);
assert!(
alpha_at(&px, w, 20, 32) > 0,
"a selected plane pixel must still be painted through the marquee after crop"
);
assert_eq!(
alpha_at(&px, w, 40, 32),
0,
"an unselected plane pixel (inside the new window) must stay transparent"
);
}
#[test]
fn crop_preserves_off_window_layer_pixels() {
let (w, h) = (64u32, 64u32);
let mut engine = test_engine(w, h);
let layer_id = engine.add_raster_layer(None);
paint_row(&mut engine, layer_id, 10.0, 0.0, w as f32 - 1.0);
let before = engine.test_readback_layer(layer_id);
let painted_x = (4..60)
.find(|&x| alpha_at(&before, w, x, 10) > 0)
.expect("row should have paint before crop");
engine.resize_canvas(CanvasRect::from_xywh(0, 20, 40, 30));
let after = engine.test_readback_layer(layer_id);
assert!(
alpha_at(&after, w, painted_x, 10) > 0,
"off-window layer pixels must be preserved on the layer across a crop"
);
}
#[test]
fn resize_canvas_undo_restores_window() {
let (w, h) = (64u32, 64u32);
let mut engine = test_engine(w, h);
let _layer = engine.add_raster_layer(None);
engine.resize_canvas(CanvasRect::from_xywh(12, 8, 30, 40));
assert_eq!(engine.canvas_rect(), CanvasRect::from_xywh(12, 8, 30, 40));
engine.undo();
assert_eq!(engine.canvas_rect(), CanvasRect::from_xywh(0, 0, w, h));
engine.redo();
assert_eq!(engine.canvas_rect(), CanvasRect::from_xywh(12, 8, 30, 40));
}
#[test]
fn resize_canvas_pure_translation_moves_window_and_undoes() {
let (w, h) = (64u32, 64u32);
let mut engine = test_engine(w, h);
let _layer = engine.add_raster_layer(None);
engine.resize_canvas(CanvasRect::from_xywh(10, 6, w, h));
assert_eq!(engine.canvas_rect(), CanvasRect::from_xywh(10, 6, w, h));
engine.undo();
assert_eq!(engine.canvas_rect(), CanvasRect::from_xywh(0, 0, w, h));
}
#[test]
fn resize_rebuilds_view_transform_for_new_dims() {
let (w, h) = (64u32, 64u32);
let mut engine = test_engine(w, h);
let _layer = engine.add_raster_layer(None);
let (sw, sh) = (200.0_f32, 200.0_f32);
engine.set_view_transform(0.0, 0.0, 1.0, 0.0, false, sw, sh);
let (cx0, cy0) = engine.screen_to_plane(sw / 2.0, sh / 2.0);
assert!(
(cx0 - 32.0).abs() < 1e-2 && (cy0 - 32.0).abs() < 1e-2,
"pre-resize screen center should map to (32, 32), got ({cx0}, {cy0})"
);
engine.resize_canvas(CanvasRect::from_xywh(0, 0, 128, 96));
let (cx1, cy1) = engine.screen_to_plane(sw / 2.0, sh / 2.0);
assert!(
(cx1 - 64.0).abs() < 1e-2,
"screen center must map to the NEW canvas center x (64) after resize, got {cx1}"
);
assert!(
(cy1 - 48.0).abs() < 1e-2,
"screen center must map to the NEW canvas center y (48) after resize, got {cy1}"
);
engine.undo();
let (cx2, cy2) = engine.screen_to_plane(sw / 2.0, sh / 2.0);
assert!(
(cx2 - 32.0).abs() < 1e-2 && (cy2 - 32.0).abs() < 1e-2,
"after undo, screen center must map back to (32, 32), got ({cx2}, {cy2})"
);
}
#[test]
fn screen_to_plane_includes_canvas_origin() {
let (w, h) = (64u32, 64u32);
let mut engine = test_engine(w, h);
let _layer = engine.add_raster_layer(None);
let (sw, sh) = (200.0_f32, 200.0_f32);
engine.set_view_transform(0.0, 0.0, 1.0, 0.0, false, sw, sh);
engine.resize_canvas(CanvasRect::from_xywh(8, 4, 40, 40));
let (px, py) = engine.screen_to_plane(sw / 2.0, sh / 2.0);
assert!(
(px - 28.0).abs() < 1e-2,
"screen center must map to plane x = origin.x + window_center (28), got {px}"
);
assert!(
(py - 24.0).abs() < 1e-2,
"screen center must map to plane y = origin.y + window_center (24), got {py}"
);
}
fn ants_bbox(prims: &[OverlayPrimitive]) -> (f32, f32, f32, f32) {
let (mut minx, mut miny, mut maxx, mut maxy) = (f32::MAX, f32::MAX, f32::MIN, f32::MIN);
let mut found = false;
for p in prims {
if p.kind != KIND_DASHED_LINE || (p.flags & FLAG_CANVAS_SPACE) == 0 {
continue;
}
for &[x, y] in &[p.p0, p.p1] {
found = true;
minx = minx.min(x);
miny = miny.min(y);
maxx = maxx.max(x);
maxy = maxy.max(y);
}
}
assert!(found, "no marching-ants primitives present");
(minx, miny, maxx, maxy)
}
#[test]
fn marching_ants_track_selection_plane_bounds_after_crop() {
let (w, h) = (64u32, 64u32);
let mut engine = test_engine(w, h);
let _layer = engine.add_raster_layer(None);
engine.resize_canvas(CanvasRect::from_xywh(8, 4, 40, 40));
engine.select_rect(16.0, 12.0, 10.0, 8.0, SelectionMode::Replace, false, 0.0);
let (minx, miny, maxx, maxy) = ants_bbox(&engine.test_selection_overlay());
assert!(
(minx - 16.0).abs() <= 2.0 && (miny - 12.0).abs() <= 2.0,
"ants min ({minx}, {miny}) must track the selection's PLANE origin (16, 12), \
not the window-local (8, 8)"
);
assert!(
(maxx - 26.0).abs() <= 2.0 && (maxy - 20.0).abs() <= 2.0,
"ants max ({maxx}, {maxy}) must track the selection's PLANE far edge (26, 20)"
);
}
#[test]
fn transform_from_selection_uses_plane_source_origin() {
let (w, h) = (64u32, 64u32);
let mut engine = test_engine(w, h);
let layer_id = engine.add_raster_layer(None);
paint_row(&mut engine, layer_id, 16.0, 8.0, 40.0);
engine.resize_canvas(CanvasRect::from_xywh(8, 4, 40, 40));
engine.select_rect(16.0, 12.0, 10.0, 8.0, SelectionMode::Replace, false, 0.0);
assert!(engine.begin_transform(layer_id), "transform should set up");
let (sox, soy, sw, sh, _m) = engine.floating_info().expect("floating active");
assert!(
(sox - 16.0).abs() <= 1.0 && (soy - 12.0).abs() <= 1.0,
"source origin ({sox}, {soy}) must be the selection's PLANE origin (16, 12), \
not the window-local (8, 8)"
);
assert!(
(sw - 10.0).abs() <= 1.0 && (sh - 8.0).abs() <= 1.0,
"source size ({sw}, {sh}) should match the selection (10, 8)"
);
}
#[test]
fn transform_preview_matches_pretransform_present_after_crop() {
let (w, h) = (96u32, 96u32);
let (vw, vh) = (192u32, 192u32);
let mut engine = test_engine(w, h);
let layer_id = engine.add_raster_layer(None);
engine.set_view_transform(0.0, 0.0, 1.0, 0.0, false, vw as f32, vh as f32);
engine.resize_canvas(CanvasRect::from_xywh(24, 16, 48, 48));
paint_cross(&mut engine, layer_id, 48.0, 40.0, 6.0);
let baseline = engine.test_readback_viewport(vw, vh);
let (bx, by) = red_centroid(&baseline, vw, vh);
engine.select_rect(40.0, 32.0, 16.0, 16.0, SelectionMode::Replace, false, 0.0);
assert!(engine.begin_transform(layer_id), "transform should set up");
let preview = engine.test_readback_viewport(vw, vh);
let (px, py) = red_centroid(&preview, vw, vh);
assert!(
(px - bx).abs() <= 2.0 && (py - by).abs() <= 2.0,
"identity-transform preview moved the content: baseline centroid ({bx:.1}, {by:.1}) \
vs preview ({px:.1}, {py:.1}) — a `canvas_origin` shift in the floating preview frame"
);
}
fn red_centroid(px: &[u8], w: u32, h: u32) -> (f32, f32) {
let (mut sx, mut sy, mut n) = (0.0f32, 0.0f32, 0u32);
for y in 0..h {
for x in 0..w {
let i = ((y * w + x) * 4) as usize;
if px[i] > 128 && px[i + 1] < 80 && px[i + 2] < 80 {
sx += x as f32;
sy += y as f32;
n += 1;
}
}
}
assert!(n > 0, "no red pixels found");
(sx / n as f32, sy / n as f32)
}
#[test]
fn copy_selection_after_crop_extracts_plane_pixels_and_offset() {
let (w, h) = (64u32, 64u32);
let mut engine = test_engine(w, h);
let mut rgba = vec![0u8; (w * h * 4) as usize];
for y in 28..36 {
for x in 30..38 {
let i = ((y * w + x) * 4) as usize;
rgba[i] = 255; rgba[i + 3] = 255; }
}
let layer_id = engine.paste_image(w, h, &rgba, 0, 0, None);
engine.resize_canvas(CanvasRect::from_xywh(16, 12, 40, 40));
engine.select_rect(30.0, 28.0, 8.0, 8.0, SelectionMode::Replace, false, 0.0);
engine.copy(layer_id);
let mut export = None;
for _ in 0..8 {
engine.test_flush_readbacks();
engine.render(0.0);
if let Some(e) = engine.poll_copy_result() {
export = Some(e);
break;
}
}
let export = export.expect("copy readback should complete");
assert_eq!(
(export.offset_x, export.offset_y),
(30, 28),
"clipboard offset must be the selection's plane position"
);
let any_red = export
.rgba
.chunks_exact(4)
.any(|p| p[0] > 200 && p[1] < 60 && p[2] < 60 && p[3] > 200);
assert!(
any_red,
"copied pixels must be the red square; an offset layer read would grab empty space"
);
}
#[test]
fn identity_transform_with_selection_preserves_content_after_crop() {
let (w, h) = (96u32, 96u32);
let mut engine = test_engine(w, h);
let mut rgba = vec![0u8; (w * h * 4) as usize];
for y in 32..48 {
for x in 40..56 {
let i = ((y * w + x) * 4) as usize;
rgba[i] = 255;
rgba[i + 3] = 255;
}
}
let layer_id = engine.paste_image(w, h, &rgba, 0, 0, None);
engine.resize_canvas(CanvasRect::from_xywh(24, 16, 48, 48));
engine.select_rect(40.0, 32.0, 16.0, 16.0, SelectionMode::Replace, false, 0.0);
assert!(engine.begin_transform(layer_id), "transform should set up");
engine.commit_floating();
engine.render(0.0);
let px = engine.test_readback_layer(layer_id);
assert!(
alpha_at(&px, w, 47, 40) > 0 && alpha_at(&px, w, 41, 33) > 0,
"identity transform-with-selection erased the content after a crop \
(cropped selection mask read at the wrong frame)"
);
}
#[test]
fn pick_color_merged_reads_plane_pixel_after_crop() {
use darkly::engine::PickSource;
let (w, h) = (64u32, 64u32);
let mut engine = test_engine(w, h);
let mut rgba = vec![0u8; (w * h * 4) as usize];
for px in rgba.chunks_exact_mut(4) {
px.copy_from_slice(&[200, 0, 0, 255]);
}
for y in 40..44 {
for x in 40..44 {
let i = ((y * w + x) * 4) as usize;
rgba[i..i + 4].copy_from_slice(&[0, 200, 0, 255]);
}
}
let _layer = engine.paste_image(w, h, &rgba, 0, 0, None);
engine.resize_canvas(CanvasRect::from_xywh(16, 12, 40, 40));
let _ = engine.test_readback_canvas();
engine.pick_color(41.0, 41.0, PickSource::Merged);
engine.test_flush_readbacks();
let c = engine.last_picked_color();
assert!(
c[1] > 150 && c[0] < 80,
"merged pick after crop must read the plane pixel (green); got {c:?} \
(pre-fix read the wrong composite texel or returned black)"
);
}
#[test]
fn selection_bounds_repopulate_after_crop() {
let (w, h) = (64u32, 64u32);
let mut engine = test_engine(w, h);
let mut rgba = vec![0u8; (w * h * 4) as usize];
for y in 40..48 {
for x in 40..48 {
let i = ((y * w + x) * 4) as usize;
rgba[i] = 255;
rgba[i + 3] = 255;
}
}
let layer_id = engine.paste_image(w, h, &rgba, 0, 0, None);
engine.select_rect(40.0, 40.0, 8.0, 8.0, SelectionMode::Replace, false, 0.0);
engine.resize_canvas(CanvasRect::from_xywh(16, 12, 40, 40));
for _ in 0..8 {
engine.test_flush_readbacks();
engine.render(0.0);
if engine.test_selection_cpu_cache().is_some() {
break;
}
}
assert!(engine.begin_transform(layer_id), "transform should set up");
let (sox, soy, _, _, _) = engine.floating_info().expect("floating active");
assert!(
(sox - 40.0).abs() <= 2.0 && (soy - 40.0).abs() <= 2.0,
"post-crop selection bounds are stale: transform source ({sox}, {soy}) \
should be the plane position (40, 40)"
);
}
#[test]
fn crop_to_selection_matches_selection_bounds() {
let (w, h) = (64u32, 64u32);
let mut engine = test_engine(w, h);
let _layer = engine.add_raster_layer(None);
engine.select_rect(16.0, 12.0, 20.0, 24.0, SelectionMode::Replace, false, 0.0);
engine.crop_to_selection();
assert_eq!(engine.canvas_rect(), CanvasRect::from_xywh(16, 12, 20, 24));
}
fn paint_cross(engine: &mut DarklyEngine, layer_id: LayerId, cx: f32, cy: f32, arm: f32) {
for horizontal in [true, false] {
engine.begin_stroke(layer_id);
let steps = 48;
for i in 0..=steps {
let t = i as f32 / steps as f32;
let (x, y) = if horizontal {
(cx - arm + 2.0 * arm * t, cy)
} else {
(cx, cy - arm + 2.0 * arm * t)
};
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: i as f64 * 16.0,
cr: 1.0,
cg: 0.0,
cb: 0.0,
ca: 1.0,
});
}
engine.end_stroke();
}
}
fn red_extent(px: &[u8], w: u32, h: u32) -> (u32, u32) {
let (mut minx, mut miny, mut maxx, mut maxy) = (u32::MAX, u32::MAX, 0u32, 0u32);
let mut found = false;
for y in 0..h {
for x in 0..w {
let i = ((y * w + x) * 4) as usize;
if px[i] > 128 && px[i + 1] < 80 && px[i + 2] < 80 {
found = true;
minx = minx.min(x);
maxx = maxx.max(x);
miny = miny.min(y);
maxy = maxy.max(y);
}
}
}
assert!(found, "no red cross found");
(maxx - minx + 1, maxy - miny + 1)
}
fn presented_cross_ratio(crop: Option<CanvasRect>) -> f32 {
let (w, h) = (200u32, 200u32);
let (vw, vh) = (400u32, 400u32);
let mut engine = test_engine(w, h);
let layer_id = engine.add_raster_layer(None);
engine.set_view_transform(0.0, 0.0, 1.0, 0.0, false, vw as f32, vh as f32);
let center = match crop {
Some(rect) => {
engine.resize_canvas(rect);
(
rect.origin.x as f32 + rect.width as f32 / 2.0,
rect.origin.y as f32 + rect.height as f32 / 2.0,
)
}
None => (100.0, 100.0),
};
paint_cross(&mut engine, layer_id, center.0, center.1, 8.0);
let px = engine.test_readback_viewport(vw, vh);
let (cw, ch) = red_extent(&px, vw, vh);
cw as f32 / ch as f32
}
#[test]
fn crop_does_not_anisotropically_squash_presented_content() {
let uncropped = presented_cross_ratio(None);
let cropped = presented_cross_ratio(Some(CanvasRect::from_xywh(40, 30, 160, 100)));
let rel = (cropped / uncropped - 1.0).abs();
assert!(
rel < 0.08,
"crop squashed the presented image: arm ratio {cropped:.3} vs uncropped {uncropped:.3} \
({:.0}% change)",
rel * 100.0
);
}
#[test]
fn successive_crops_do_not_compound_squash() {
let uncropped = presented_cross_ratio(None);
let one = presented_cross_ratio(Some(CanvasRect::from_xywh(40, 30, 160, 100)));
let (vw, vh) = (400u32, 400u32);
let mut engine = test_engine(200, 200);
let layer_id = engine.add_raster_layer(None);
engine.set_view_transform(0.0, 0.0, 1.0, 0.0, false, vw as f32, vh as f32);
engine.resize_canvas(CanvasRect::from_xywh(40, 30, 160, 100));
engine.resize_canvas(CanvasRect::from_xywh(60, 50, 110, 70));
paint_cross(&mut engine, layer_id, 115.0, 85.0, 8.0);
let px = engine.test_readback_viewport(vw, vh);
let (cw, ch) = red_extent(&px, vw, vh);
let two = cw as f32 / ch as f32;
assert!(
(one / uncropped - 1.0).abs() < 0.08 && (two / uncropped - 1.0).abs() < 0.08,
"squash compounds across crops: uncropped {uncropped:.3}, 1 crop {one:.3}, 2 crops {two:.3}"
);
}