Merge branch 'worktree-agent-a22a049c461818dbe' into integration

# Conflicts:
#	core/dr-pipeline/tests/mask_sidecar.rs
This commit is contained in:
2026-08-22 13:23:34 +02:00
14 changed files with 2352 additions and 196 deletions
+132
View File
@@ -189,3 +189,135 @@ fn the_mask_covers_the_same_fraction_at_every_size() {
"and that share is the left half: {small:.3}"
);
}
// ---------------------------------------------------------------------------
// Gradient geometry
//
// The mask is rasterised over the source frame, whose two axes are not the
// same length. A gradient measured in raw 0..1 fractions therefore means a
// different distance horizontally than vertically — so a circle comes out an
// ellipse and an angle is not the angle asked for. Neither shows in the stored
// numbers, and both are what a photographer is looking straight at while
// dragging a handle.
// ---------------------------------------------------------------------------
/// A flat grey frame at an arbitrary shape, brightened wherever `stack` covers.
fn render_gradient(ctx: &GpuContext, stack: &MaskStack, w: u32, h: u32) -> Vec<u8> {
let data: Vec<u8> = (0..w * h).flat_map(|_| [128u8, 128, 128, 255]).collect();
let source = DemosaicedImage::from_rgba8(ctx, &data, w, h).expect("upload");
let mut masks = MaskPass::new(ctx).expect("mask pass");
let array = masks.render(stack, None, None, w, h).expect("rasterise");
let shader = compose_full(&ops::chain(), &Framing::new(), ColourSpace::Srgb, stack);
let mut adjust = AdjustPass::new(ctx);
adjust
.render_masked(&source, &shader, w, h, Some(array))
.expect("render");
adjust.export_pixels().expect("readback").0
}
fn brightened(pixels: &[u8], w: u32, x: u32, y: u32) -> bool {
pixels[((y * w + x) * 4) as usize] > 160
}
fn gradient(source: MaskSource) -> MaskStack {
let mut stack = MaskStack::new();
let mut layer = MaskLayer::new("g1", source);
layer.set_param("exposure", ParamId("exposure"), 2.0);
stack.push(layer);
stack
}
/// A radial with equal radii must be round on the screen, not on the numbers.
#[test]
fn a_radial_with_equal_radii_is_a_circle_on_a_wide_frame() {
let Some(ctx) = ctx() else {
eprintln!("no adapter; skipping");
return;
};
// 3:2. On a square frame this test cannot fail, which is why it is not one.
const W: u32 = 96;
const H: u32 = 64;
// 0.3 of the frame's height. In pixels that is 19 either way — the x axis
// spans 0..1.5 in the same units, so the fraction is smaller and the
// distance is the same.
let pixels = render_gradient(
&ctx,
&gradient(MaskSource::Radial {
centre: (0.5, 0.5),
radii: (0.3, 0.3),
angle: 0.0,
// Hard, so "covered" is a question with an answer rather than a
// ramp to pick a threshold out of.
feather: 0.0,
}),
W,
H,
);
let (cx, cy) = (W / 2, H / 2);
assert!(brightened(&pixels, W, cx, cy), "the centre must be covered");
for (dx, dy, what) in [(15u32, 0u32, "right"), (0, 15, "down")] {
assert!(
brightened(&pixels, W, cx + dx, cy + dy),
"15px {what} of centre is inside a 19px radius"
);
}
for (dx, dy, what) in [(24u32, 0u32, "right"), (0, 24, "down")] {
assert!(
!brightened(&pixels, W, cx + dx, cy + dy),
"24px {what} of centre is outside it — before the aspect \
correction the horizontal reach was 28px and this passed only \
downwards"
);
}
}
/// And a ramp at 45° must be at 45° where the photographer sees it.
#[test]
fn a_diagonal_ramp_runs_at_the_angle_it_was_given() {
let Some(ctx) = ctx() else {
eprintln!("no adapter; skipping");
return;
};
const W: u32 = 96;
const H: u32 = 64;
let pixels = render_gradient(
&ctx,
&gradient(MaskSource::Linear {
centre: (0.5, 0.5),
angle: std::f32::consts::FRAC_PI_4,
width: 0.0,
}),
W,
H,
);
// Coverage increases along (cos 45°, sin 45°), so the half-way line runs
// from lower-left to upper-right through the centre: equal steps right and
// down stay on the covered side, and the two sides of it disagree.
let (cx, cy) = (W / 2, H / 2);
assert!(
brightened(&pixels, W, cx + 16, cy + 16),
"down and to the right of the line is inside"
);
assert!(
!brightened(&pixels, W, cx - 16, cy - 16),
"up and to the left of it is outside"
);
// The diagonal itself, sampled a few pixels either side. Without the
// aspect correction the line comes out at 34 degrees and both of these
// land on the same side of it.
assert!(
brightened(&pixels, W, cx + 18, cy - 14),
"just below the 45 degree line is inside"
);
assert!(
!brightened(&pixels, W, cx + 14, cy - 18),
"just above it is not"
);
}