Format the tree the way the gate asks for it
`cargo fmt --check` is a required step and had drifted across 45 files. Most of it arrived this week: several operations were written in parallel worktrees and merged by hand, and a hand-merge resolves conflicts without ever running the formatter over the result. No behaviour changes — this is `cargo fmt --all` and nothing else, kept as its own commit so the next reader can skip it wholesale rather than search it for one that matters. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -487,7 +487,8 @@ mod tests {
|
||||
// And the last span is shallower than the one before it, which is
|
||||
// what a shoulder *is*. Without one the curve clips highlights
|
||||
// harder than the linear rendering did.
|
||||
let slope = |i: usize| (curve.ys[i + 1] - curve.ys[i]) / (curve.xs[i + 1] - curve.xs[i]);
|
||||
let slope =
|
||||
|i: usize| (curve.ys[i + 1] - curve.ys[i]) / (curve.xs[i + 1] - curve.xs[i]);
|
||||
assert!(
|
||||
slope(POINTS - 2) < slope(POINTS - 3),
|
||||
"{name} has no highlight shoulder"
|
||||
@@ -502,13 +503,7 @@ mod tests {
|
||||
// as a dark halo in a gradient, which reads as a rendering fault
|
||||
// rather than as a bad profile.
|
||||
assert_eq!(
|
||||
BaseCurve::from_points(&[
|
||||
[0.0, 0.0],
|
||||
[0.25, 0.4],
|
||||
[0.5, 0.3],
|
||||
[0.75, 0.8],
|
||||
[1.0, 1.0]
|
||||
]),
|
||||
BaseCurve::from_points(&[[0.0, 0.0], [0.25, 0.4], [0.5, 0.3], [0.75, 0.8], [1.0, 1.0]]),
|
||||
None
|
||||
);
|
||||
}
|
||||
@@ -540,13 +535,7 @@ mod tests {
|
||||
// point above 1.0 would put the shoulder outside the range the curve
|
||||
// is defined over and silently flatten everything below it.
|
||||
assert_eq!(
|
||||
BaseCurve::from_points(&[
|
||||
[0.0, 0.0],
|
||||
[0.25, 0.3],
|
||||
[0.5, 1.4],
|
||||
[0.75, 1.5],
|
||||
[1.0, 1.6]
|
||||
]),
|
||||
BaseCurve::from_points(&[[0.0, 0.0], [0.25, 0.3], [0.5, 1.4], [0.75, 1.5], [1.0, 1.6]]),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
@@ -375,7 +375,10 @@ fn rawler_location(gps: &rawler::exif::ExifGPS) -> Option<Location> {
|
||||
(r.d != 0).then(|| r.n as f64 / r.d as f64)
|
||||
}
|
||||
|
||||
fn degrees(dms: &[rawler::formats::tiff::Rational; 3], reference: Option<&String>) -> Option<f64> {
|
||||
fn degrees(
|
||||
dms: &[rawler::formats::tiff::Rational; 3],
|
||||
reference: Option<&String>,
|
||||
) -> Option<f64> {
|
||||
let d = ratio(&dms[0])? + ratio(&dms[1])? / 60.0 + ratio(&dms[2])? / 3600.0;
|
||||
// South and west are stored as positive magnitudes with a letter.
|
||||
let negative = matches!(
|
||||
@@ -534,7 +537,10 @@ pub fn decode(bytes: &[u8]) -> Result<RawImage, DecodeError> {
|
||||
// both callers ask the same object for it.
|
||||
let profile = profile::CameraProfile::extract(&image, &dng);
|
||||
let color_matrix = profile.as_ref().and_then(|p| p.cam_to_srgb());
|
||||
let wb_coeffs = sane_wb(image.wb_coeffs, profile.as_ref().map(|p| p.xyz_to_cam()).as_ref());
|
||||
let wb_coeffs = sane_wb(
|
||||
image.wb_coeffs,
|
||||
profile.as_ref().map(|p| p.xyz_to_cam()).as_ref(),
|
||||
);
|
||||
|
||||
// The rendering half of the profile (FR-DEV-3e). rawler's cleaned strings
|
||||
// are preferred where it has them — they are what the shipped database is
|
||||
|
||||
@@ -678,7 +678,8 @@ fn read_gps_entries(r: &TiffReader, entries: &[Entry]) -> Option<dr_types::Locat
|
||||
// still writes the entry.
|
||||
let degrees = |tag: u16, ref_tag: u16| -> Option<f64> {
|
||||
let e = find(tag)?;
|
||||
let d = r.rational(e, 0)? + r.rational(e, 1).unwrap_or(0.0) / 60.0
|
||||
let d = r.rational(e, 0)?
|
||||
+ r.rational(e, 1).unwrap_or(0.0) / 60.0
|
||||
+ r.rational(e, 2).unwrap_or(0.0) / 3600.0;
|
||||
// The magnitude is unsigned; the hemisphere is a letter beside it.
|
||||
let south_or_west = find(ref_tag)
|
||||
@@ -1412,7 +1413,9 @@ pub fn defects(tiff_data: &[u8]) -> Defects {
|
||||
/// skip one this build does not implement rather than abandoning the list —
|
||||
/// and lists mixing a warp with a defect map are ordinary.
|
||||
fn read_opcode_list(bytes: &[u8], out: &mut Defects) {
|
||||
let Some(count) = be_u32(bytes, 0) else { return };
|
||||
let Some(count) = be_u32(bytes, 0) else {
|
||||
return;
|
||||
};
|
||||
// A sensor has a handful of opcodes, not thousands. A huge count is a
|
||||
// corrupt or hostile file.
|
||||
if count > 256 {
|
||||
|
||||
@@ -1048,7 +1048,11 @@ mod tests {
|
||||
// would render black, which is much harder to diagnose than
|
||||
// uncalibrated.
|
||||
assert!(!usable(&[[0.0; 3]; 3]));
|
||||
assert!(!usable(&[[f32::NAN, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]));
|
||||
assert!(!usable(&[
|
||||
[f32::NAN, 0.0, 0.0],
|
||||
[0.0, 1.0, 0.0],
|
||||
[0.0, 0.0, 1.0]
|
||||
]));
|
||||
assert!(usable(&SIX_D_D65));
|
||||
}
|
||||
|
||||
|
||||
@@ -77,9 +77,7 @@ pub fn encode(
|
||||
.map(|m| m.sanitised(settings.strip_location));
|
||||
// JPEG and PNG take a finished block; TIFF writes the tags into its own
|
||||
// directory and needs the fields.
|
||||
let block = carried
|
||||
.as_ref()
|
||||
.and_then(|m| exif::block(m, width, height));
|
||||
let block = carried.as_ref().and_then(|m| exif::block(m, width, height));
|
||||
|
||||
match settings.format {
|
||||
ExportFormat::Jpeg => jpeg(
|
||||
@@ -303,7 +301,10 @@ where
|
||||
W: std::io::Write + std::io::Seek,
|
||||
K: tiff::encoder::TiffKind,
|
||||
{
|
||||
dir.write_tag(tiff::tags::Tag::Unknown(TAG_ICC_PROFILE), Undefined(profile))
|
||||
dir.write_tag(
|
||||
tiff::tags::Tag::Unknown(TAG_ICC_PROFILE),
|
||||
Undefined(profile),
|
||||
)
|
||||
.map_err(|e| ExportError::Encode(e.to_string()))
|
||||
}
|
||||
|
||||
@@ -376,7 +377,10 @@ where
|
||||
)?;
|
||||
}
|
||||
if let Some(f) = md.aperture.filter(|f| *f > 0.0) {
|
||||
dir.write_tag(Tag::Unknown(exif::tag::FNUMBER), Rationals(&[exif::tenths(f)]))?;
|
||||
dir.write_tag(
|
||||
Tag::Unknown(exif::tag::FNUMBER),
|
||||
Rationals(&[exif::tenths(f)]),
|
||||
)?;
|
||||
}
|
||||
if let Some(f) = md.focal_length.filter(|f| *f > 0.0) {
|
||||
dir.write_tag(
|
||||
@@ -935,11 +939,16 @@ mod tests {
|
||||
assert_eq!(md.aperture, Some(2.8), "{format:?}");
|
||||
assert_eq!(md.focal_length, Some(85.0), "{format:?}");
|
||||
|
||||
let loc = md.location.unwrap_or_else(|| panic!("{format:?} lost the fix"));
|
||||
let loc = md
|
||||
.location
|
||||
.unwrap_or_else(|| panic!("{format:?} lost the fix"));
|
||||
// Within a metre of where it started, which is finer than any
|
||||
// consumer receiver and far finer than the tag's own rounding.
|
||||
assert!((loc.latitude - LATITUDE).abs() < 1e-5, "{format:?} {loc:?}");
|
||||
assert!((loc.longitude - LONGITUDE).abs() < 1e-5, "{format:?} {loc:?}");
|
||||
assert!(
|
||||
(loc.longitude - LONGITUDE).abs() < 1e-5,
|
||||
"{format:?} {loc:?}"
|
||||
);
|
||||
assert_eq!(loc.altitude, Some(35.0), "{format:?}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -250,7 +250,13 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn png_export_produces_a_png() {
|
||||
let out = export(&frame(32, 32), &settings(ExportFormat::Png), "a.png".into(), None).unwrap();
|
||||
let out = export(
|
||||
&frame(32, 32),
|
||||
&settings(ExportFormat::Png),
|
||||
"a.png".into(),
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(&out.bytes[..8], b"\x89PNG\r\n\x1a\n");
|
||||
}
|
||||
|
||||
@@ -270,8 +276,20 @@ mod tests {
|
||||
fn a_sixteen_bit_tiff_is_larger_than_an_eight_bit_one() {
|
||||
// Both are uncompressed RGB; the only difference is the sample width,
|
||||
// so this is what proves the 16-bit path is not quietly writing 8.
|
||||
let eight = export(&frame(16, 16), &settings(ExportFormat::Tiff8), "a".into(), None).unwrap();
|
||||
let sixteen = export(&frame(16, 16), &settings(ExportFormat::Tiff16), "a".into(), None).unwrap();
|
||||
let eight = export(
|
||||
&frame(16, 16),
|
||||
&settings(ExportFormat::Tiff8),
|
||||
"a".into(),
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
let sixteen = export(
|
||||
&frame(16, 16),
|
||||
&settings(ExportFormat::Tiff16),
|
||||
"a".into(),
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
assert!(sixteen.bytes.len() > eight.bytes.len());
|
||||
}
|
||||
|
||||
|
||||
@@ -26,9 +26,7 @@
|
||||
//! mask pinned to the viewport, a slice read from the wrong layer — shows up
|
||||
//! here as an adjustment in the wrong place, and nowhere else.
|
||||
|
||||
use dr_gpu::{
|
||||
AdjustPass, DemosaicedImage, Demosaicer, GpuContext, MaskPass, SubjectMasks,
|
||||
};
|
||||
use dr_gpu::{AdjustPass, DemosaicedImage, Demosaicer, GpuContext, MaskPass, SubjectMasks};
|
||||
use dr_pipeline::descriptor::ParamId;
|
||||
use dr_pipeline::mask::{MaskLayer, MaskSource, MaskStack, Morphology};
|
||||
use dr_pipeline::operation::compose_full;
|
||||
@@ -152,7 +150,18 @@ fn main() {
|
||||
drain.feather = 0.02;
|
||||
pop.push(drain);
|
||||
|
||||
render_stack(&ctx, &source, &mut masks, &mut adjust, &pop, &alpha, pw, ph, ow, oh);
|
||||
render_stack(
|
||||
&ctx,
|
||||
&source,
|
||||
&mut masks,
|
||||
&mut adjust,
|
||||
&pop,
|
||||
&alpha,
|
||||
pw,
|
||||
ph,
|
||||
ow,
|
||||
oh,
|
||||
);
|
||||
write(&format!("{prefix}-colour-pop.ppm"), &adjust);
|
||||
|
||||
// ---- 2. lift the subject out of its background ------------------------
|
||||
@@ -170,7 +179,18 @@ fn main() {
|
||||
darker.feather = 0.03;
|
||||
lift.push(darker);
|
||||
|
||||
render_stack(&ctx, &source, &mut masks, &mut adjust, &lift, &alpha, pw, ph, ow, oh);
|
||||
render_stack(
|
||||
&ctx,
|
||||
&source,
|
||||
&mut masks,
|
||||
&mut adjust,
|
||||
&lift,
|
||||
&alpha,
|
||||
pw,
|
||||
ph,
|
||||
ow,
|
||||
oh,
|
||||
);
|
||||
write(&format!("{prefix}-subject-lift.ppm"), &adjust);
|
||||
|
||||
// ---- 3. the same edit, grown and shrunk -------------------------------
|
||||
@@ -192,7 +212,18 @@ fn main() {
|
||||
layer.morph_radius = radius;
|
||||
stack.push(layer);
|
||||
|
||||
render_stack(&ctx, &source, &mut masks, &mut adjust, &stack, &alpha, pw, ph, ow, oh);
|
||||
render_stack(
|
||||
&ctx,
|
||||
&source,
|
||||
&mut masks,
|
||||
&mut adjust,
|
||||
&stack,
|
||||
&alpha,
|
||||
pw,
|
||||
ph,
|
||||
ow,
|
||||
oh,
|
||||
);
|
||||
write(&format!("{prefix}-{name}.ppm"), &adjust);
|
||||
}
|
||||
|
||||
@@ -292,12 +323,18 @@ fn render_stack(
|
||||
|
||||
fn fit(w: u32, h: u32, longest: u32) -> (u32, u32) {
|
||||
let s = (longest as f32 / w.max(h) as f32).min(1.0);
|
||||
(((w as f32 * s) as u32).max(1), ((h as f32 * s) as u32).max(1))
|
||||
(
|
||||
((w as f32 * s) as u32).max(1),
|
||||
((h as f32 * s) as u32).max(1),
|
||||
)
|
||||
}
|
||||
|
||||
fn write(path: &str, adjust: &AdjustPass) {
|
||||
let (rgba, w, h) = adjust.export_pixels().expect("readback");
|
||||
let rgb: Vec<u8> = rgba.chunks_exact(4).flat_map(|p| [p[0], p[1], p[2]]).collect();
|
||||
let rgb: Vec<u8> = rgba
|
||||
.chunks_exact(4)
|
||||
.flat_map(|p| [p[0], p[1], p[2]])
|
||||
.collect();
|
||||
write_ppm(path, &rgb, w, h);
|
||||
}
|
||||
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
//! PPM for the same reason `develop` uses it: no encoder dependency, and
|
||||
//! every viewer reads it. This is a diagnostic, not an export path.
|
||||
|
||||
use dr_segment::{MergeTree, RegionField};
|
||||
use dr_gpu::{DemosaicedImage, Demosaicer, GpuContext, SegmentOptions, SegmentPass};
|
||||
use dr_segment::{MergeTree, RegionField};
|
||||
|
||||
/// The ladder the example dumps. Chosen to span "far too fine to be useful"
|
||||
/// through "one or two objects", because both ends are informative: if no rung
|
||||
|
||||
@@ -589,8 +589,8 @@ impl AdjustPass {
|
||||
});
|
||||
|
||||
if !reuse {
|
||||
let params_buf = self
|
||||
.ctx
|
||||
let params_buf =
|
||||
self.ctx
|
||||
.device
|
||||
.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: Some("adjust-params"),
|
||||
|
||||
@@ -123,8 +123,7 @@ impl Intermediates {
|
||||
// output these are never handed to a compositor, and no
|
||||
// COPY_SRC, because nothing reads them back — that is the
|
||||
// point (ARCH §6.1).
|
||||
usage: wgpu::TextureUsages::STORAGE_BINDING
|
||||
| wgpu::TextureUsages::TEXTURE_BINDING,
|
||||
usage: wgpu::TextureUsages::STORAGE_BINDING | wgpu::TextureUsages::TEXTURE_BINDING,
|
||||
view_formats: &[],
|
||||
});
|
||||
let view = texture.create_view(&Default::default());
|
||||
|
||||
@@ -30,8 +30,8 @@ pub use adjust::AdjustPass;
|
||||
// rather than an implementation detail: a detail pass is guaranteed linear,
|
||||
// unclipped, full internal precision (FR-DEV-2), and anyone reasoning about
|
||||
// VRAM at 24 MP needs to know what an intermediate costs.
|
||||
pub use detail::INTERMEDIATE_FORMAT as DETAIL_INTERMEDIATE_FORMAT;
|
||||
pub use demosaic::{DemosaicedImage, Demosaicer};
|
||||
pub use detail::INTERMEDIATE_FORMAT as DETAIL_INTERMEDIATE_FORMAT;
|
||||
pub use error::GpuError;
|
||||
// Renamed on the way out: `BINS` says enough inside `histogram`, and nothing
|
||||
// at all at a crate root shared with demosaic and segmentation.
|
||||
|
||||
+25
-14
@@ -582,10 +582,7 @@ impl MaskPass {
|
||||
MaskSource::Subject { .. } => match subjects.filter(|s| slot < s.len()) {
|
||||
Some(s) => (s, slot),
|
||||
None => {
|
||||
log::warn!(
|
||||
"mask layer {} has no distance field; skipping",
|
||||
layer.id
|
||||
);
|
||||
log::warn!("mask layer {} has no distance field; skipping", layer.id);
|
||||
continue;
|
||||
}
|
||||
},
|
||||
@@ -599,7 +596,14 @@ impl MaskPass {
|
||||
}
|
||||
_ => {
|
||||
let selected = self.selection_buffer(layer, field);
|
||||
self.draw(&mut encoder, slot as u32, ¶ms, field, &selected, subject);
|
||||
self.draw(
|
||||
&mut encoder,
|
||||
slot as u32,
|
||||
¶ms,
|
||||
field,
|
||||
&selected,
|
||||
subject,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -730,8 +734,8 @@ impl MaskPass {
|
||||
// Still worth beginning the pass: the slice has to be cleared, or an
|
||||
// unpainted layer would show whatever the last edit left in it.
|
||||
let bind_group = (!batch.is_empty()).then(|| {
|
||||
let params_buf = self
|
||||
.ctx
|
||||
let params_buf =
|
||||
self.ctx
|
||||
.device
|
||||
.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: Some("mask-brush-params"),
|
||||
@@ -901,10 +905,7 @@ impl MaskPass {
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 3,
|
||||
resource: wgpu::BindingResource::TextureView(
|
||||
subject
|
||||
.0
|
||||
.view(subject.1)
|
||||
.unwrap_or_else(|| {
|
||||
subject.0.view(subject.1).unwrap_or_else(|| {
|
||||
self.empty_subject.view(0).expect("placeholder exists")
|
||||
}),
|
||||
),
|
||||
@@ -949,7 +950,11 @@ impl MaskPass {
|
||||
}
|
||||
|
||||
fn ensure_array(&mut self, width: u32, height: u32, layers: u32) -> Result<(), GpuError> {
|
||||
if self.array.as_ref().is_some_and(|a| a.matches(width, height, layers)) {
|
||||
if self
|
||||
.array
|
||||
.as_ref()
|
||||
.is_some_and(|a| a.matches(width, height, layers))
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -1111,7 +1116,9 @@ mod tests {
|
||||
}));
|
||||
|
||||
let mut pass = MaskPass::new(&ctx).expect("mask pass");
|
||||
let array = pass.render(&stack, Some(&field), None, w, h).expect("render");
|
||||
let array = pass
|
||||
.render(&stack, Some(&field), None, w, h)
|
||||
.expect("render");
|
||||
assert_eq!(array.size(), (w, h));
|
||||
assert_eq!(array.layers(), 1);
|
||||
}
|
||||
@@ -1201,7 +1208,11 @@ mod tests {
|
||||
assert_eq!(batch.headers.len(), 1);
|
||||
|
||||
let h = &batch.headers[0];
|
||||
assert!(h.hi[0] - h.lo[0] > 0.2, "wide enough for the dab: {h:?}", h = (h.lo, h.hi));
|
||||
assert!(
|
||||
h.hi[0] - h.lo[0] > 0.2,
|
||||
"wide enough for the dab: {h:?}",
|
||||
h = (h.lo, h.hi)
|
||||
);
|
||||
assert!(
|
||||
h.hi[1] - h.lo[1] > h.hi[0] - h.lo[0],
|
||||
"and taller than it is wide in normalised units, since the radius \
|
||||
|
||||
@@ -62,9 +62,7 @@ fn rendered_level(ctx: &GpuContext, level: u16, curve: BaseCurve) -> u8 {
|
||||
.expect("demosaic");
|
||||
let shader = EditGraph::default_chain().compose();
|
||||
let mut adjust = AdjustPass::new(ctx);
|
||||
adjust
|
||||
.render(&source, &shader, SIZE, SIZE)
|
||||
.expect("render");
|
||||
adjust.render(&source, &shader, SIZE, SIZE).expect("render");
|
||||
let (pixels, _, _) = adjust.export_pixels().expect("readback");
|
||||
let centre = ((SIZE / 2) * SIZE + SIZE / 2) * 4;
|
||||
pixels[centre as usize]
|
||||
|
||||
@@ -84,12 +84,7 @@ fn sharpened(amount: f32, radius: f32, threshold: f32) -> EditGraph {
|
||||
/// The whole calling convention a frontend adopts, in five lines: compose both
|
||||
/// halves from one graph at one output space, ask the graph for the scale, and
|
||||
/// pass the invalidation key through.
|
||||
fn render(
|
||||
pass: &mut AdjustPass,
|
||||
graph: &EditGraph,
|
||||
source: &DemosaicedImage,
|
||||
out: u32,
|
||||
) -> Vec<u8> {
|
||||
fn render(pass: &mut AdjustPass, graph: &EditGraph, source: &DemosaicedImage, out: u32) -> Vec<u8> {
|
||||
let shader = graph.compose_for(ColourSpace::Srgb);
|
||||
let scale = graph.render_scale(source.size(), (out, out));
|
||||
let detail = graph.compose_detail_for(scale, ColourSpace::Srgb);
|
||||
@@ -281,7 +276,10 @@ fn a_proxy_and_an_export_sharpen_the_same_photograph() {
|
||||
proxy_width > 0.05 && export_width > 0.05,
|
||||
"{proxy_width:.3} / {export_width:.3}"
|
||||
);
|
||||
assert!(proxy_area > 1.0 && export_area > 1.0, "{proxy_area} / {export_area}");
|
||||
assert!(
|
||||
proxy_area > 1.0 && export_area > 1.0,
|
||||
"{proxy_area} / {export_area}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -80,9 +80,7 @@ fn expected_profile(size: u32, r: i32) -> Vec<u8> {
|
||||
let edge = (size / 2) as i32;
|
||||
(0..size as i32)
|
||||
.map(|x| {
|
||||
let white = (-r..=r)
|
||||
.filter(|i| (x + i).clamp(0, last) >= edge)
|
||||
.count();
|
||||
let white = (-r..=r).filter(|i| (x + i).clamp(0, last) >= edge).count();
|
||||
srgb_encode(white as f32 / (2 * r + 1) as f32)
|
||||
})
|
||||
.collect()
|
||||
@@ -232,7 +230,10 @@ fn an_inactive_detail_operation_costs_exactly_nothing() {
|
||||
let mut without = AdjustPass::new(&ctx);
|
||||
let b = render(&ctx, &mut without, &plain, &source, SIZE);
|
||||
|
||||
assert_eq!(a, b, "an operation at its defaults must not touch the image");
|
||||
assert_eq!(
|
||||
a, b,
|
||||
"an operation at its defaults must not touch the image"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -252,7 +253,11 @@ fn moving_a_detail_parameter_does_not_re_run_the_colour_pass() {
|
||||
graph.set_param(PROBE, RADIUS, 0.0625);
|
||||
render(&ctx, &mut pass, &graph, &source, SIZE);
|
||||
assert_eq!(pass.colour_dispatches(), 1);
|
||||
assert_eq!(pass.detail_dispatches(), 2, "a separable blur is two passes");
|
||||
assert_eq!(
|
||||
pass.detail_dispatches(),
|
||||
2,
|
||||
"a separable blur is two passes"
|
||||
);
|
||||
|
||||
// Drag the sharpening slider. The colour chain is untouched, so the linear
|
||||
// intermediate it wrote is still exactly right.
|
||||
|
||||
@@ -65,12 +65,7 @@ fn render_at(
|
||||
h: u32,
|
||||
) -> Vec<u8> {
|
||||
let source = grey_at(ctx, w, h);
|
||||
let shader = compose_full(
|
||||
&ops::chain(),
|
||||
&Framing::new(),
|
||||
ColourSpace::Srgb,
|
||||
stack,
|
||||
);
|
||||
let shader = compose_full(&ops::chain(), &Framing::new(), ColourSpace::Srgb, stack);
|
||||
|
||||
let mut masks = MaskPass::new(ctx).expect("mask pass");
|
||||
let array = masks.render(stack, field, None, w, h).expect("rasterise");
|
||||
@@ -245,11 +240,14 @@ fn stacked_layers_use_their_own_masks() {
|
||||
ids: vec![0],
|
||||
}));
|
||||
// Right half down.
|
||||
let mut darken = MaskLayer::new("m2", MaskSource::Regions {
|
||||
let mut darken = MaskLayer::new(
|
||||
"m2",
|
||||
MaskSource::Regions {
|
||||
signature: 1,
|
||||
level: 2,
|
||||
ids: vec![1],
|
||||
});
|
||||
},
|
||||
);
|
||||
darken.set_param("exposure", ParamId("exposure"), -2.0);
|
||||
stack.push(darken);
|
||||
|
||||
@@ -412,12 +410,7 @@ fn a_stroke_that_doubles_back_does_not_build_up() {
|
||||
return;
|
||||
};
|
||||
|
||||
let once = stack_of(painted(&[(
|
||||
false,
|
||||
0.15,
|
||||
0.5,
|
||||
vec![(0.1, 0.5), (0.9, 0.5)],
|
||||
)]));
|
||||
let once = stack_of(painted(&[(false, 0.15, 0.5, vec![(0.1, 0.5), (0.9, 0.5)])]));
|
||||
let twice = stack_of(painted(&[(
|
||||
false,
|
||||
0.15,
|
||||
@@ -473,10 +466,17 @@ fn a_dab_is_round_on_a_frame_that_is_not_square() {
|
||||
let pixels = render_at(&ctx, &stack, None, W, H);
|
||||
|
||||
let lit = |v: u8| v > 160;
|
||||
let across = (0..W).filter(|&x| lit(luma_in(&pixels, W, x, H / 2))).count();
|
||||
let down = (0..H).filter(|&y| lit(luma_in(&pixels, W, W / 2, y))).count();
|
||||
let across = (0..W)
|
||||
.filter(|&x| lit(luma_in(&pixels, W, x, H / 2)))
|
||||
.count();
|
||||
let down = (0..H)
|
||||
.filter(|&y| lit(luma_in(&pixels, W, W / 2, y)))
|
||||
.count();
|
||||
|
||||
assert!(across > 4 && down > 4, "the dab should exist: {across}x{down}");
|
||||
assert!(
|
||||
across > 4 && down > 4,
|
||||
"the dab should exist: {across}x{down}"
|
||||
);
|
||||
assert!(
|
||||
across.abs_diff(down) <= 2,
|
||||
"a dab must be as wide as it is tall, got {across} across and {down} down"
|
||||
|
||||
@@ -158,7 +158,12 @@ fn clarity_lifts_local_contrast_and_leaves_the_flat_regions_alone() {
|
||||
|
||||
// Far outside the kernel's reach the base equals the pixel, the detail
|
||||
// signal is zero, and the output must be the input to the last code value.
|
||||
for x in [0, reach / 2, SIZE as usize - 1 - reach / 2, SIZE as usize - 1] {
|
||||
for x in [
|
||||
0,
|
||||
reach / 2,
|
||||
SIZE as usize - 1 - reach / 2,
|
||||
SIZE as usize - 1,
|
||||
] {
|
||||
assert!(
|
||||
edited[x].abs_diff(plain[x]) <= 1,
|
||||
"column {x} moved by {} away from any edge",
|
||||
@@ -246,7 +251,10 @@ fn the_soft_limit_bounds_the_halo_at_a_hard_edge() {
|
||||
an unlimited unsharp mask would give"
|
||||
);
|
||||
// But it is still a real effect, not a control that does nothing.
|
||||
assert!(worst > 0.1, "clarity moved almost nothing: {worst:.3} stops");
|
||||
assert!(
|
||||
worst > 0.1,
|
||||
"clarity moved almost nothing: {worst:.3} stops"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -476,7 +484,11 @@ fn dragging_the_slider_re_runs_the_detail_stage_and_nothing_else() {
|
||||
let mut graph = graph_with(CLARITY, 40.0);
|
||||
render(&mut pass, &graph, &source, 256);
|
||||
assert_eq!(pass.colour_dispatches(), 1);
|
||||
assert_eq!(pass.detail_dispatches(), 2, "a separable mask is two passes");
|
||||
assert_eq!(
|
||||
pass.detail_dispatches(),
|
||||
2,
|
||||
"a separable mask is two passes"
|
||||
);
|
||||
let pipelines = pass.cached_detail_pipelines();
|
||||
|
||||
for amount in [50.0, 60.0, 70.0] {
|
||||
|
||||
@@ -25,7 +25,9 @@ fn ctx() -> Option<GpuContext> {
|
||||
}
|
||||
|
||||
fn grey(ctx: &GpuContext, size: u32) -> DemosaicedImage {
|
||||
let data: Vec<u8> = (0..size * size).flat_map(|_| [128, 128, 128, 255]).collect();
|
||||
let data: Vec<u8> = (0..size * size)
|
||||
.flat_map(|_| [128, 128, 128, 255])
|
||||
.collect();
|
||||
DemosaicedImage::from_rgba8(ctx, &data, size, size).expect("upload")
|
||||
}
|
||||
|
||||
|
||||
@@ -112,11 +112,7 @@ fn split(pixels: &[u8], width: u32, x: u32, y: u32) -> (f32, [f32; 3]) {
|
||||
}
|
||||
|
||||
/// Upload an image built from a per-pixel closure.
|
||||
fn upload(
|
||||
ctx: &GpuContext,
|
||||
size: u32,
|
||||
f: impl Fn(u32, u32) -> [u8; 3],
|
||||
) -> DemosaicedImage {
|
||||
fn upload(ctx: &GpuContext, size: u32, f: impl Fn(u32, u32) -> [u8; 3]) -> DemosaicedImage {
|
||||
let data: Vec<u8> = (0..size * size)
|
||||
.flat_map(|i| {
|
||||
let (x, y) = (i % size, i / size);
|
||||
@@ -248,7 +244,11 @@ fn modulation(
|
||||
let mut count = 0.0;
|
||||
for x in margin..(width - margin) {
|
||||
let value = sample(linear(pixels, width, x, row));
|
||||
let sign = if (x / half_period) % 2 == 0 { 1.0 } else { -1.0 };
|
||||
let sign = if (x / half_period) % 2 == 0 {
|
||||
1.0
|
||||
} else {
|
||||
-1.0
|
||||
};
|
||||
total += value * sign;
|
||||
count += 1.0;
|
||||
}
|
||||
@@ -305,7 +305,13 @@ fn chroma_noise_reduction_never_moves_lightness() {
|
||||
let mut off = AdjustPass::new(&ctx);
|
||||
let plain = render(&ctx, &mut off, &graph_with(0.0, 0.0), &source, (SIZE, SIZE));
|
||||
let mut on = AdjustPass::new(&ctx);
|
||||
let chroma = render(&ctx, &mut on, &graph_with(0.0, 100.0), &source, (SIZE, SIZE));
|
||||
let chroma = render(
|
||||
&ctx,
|
||||
&mut on,
|
||||
&graph_with(0.0, 100.0),
|
||||
&source,
|
||||
(SIZE, SIZE),
|
||||
);
|
||||
|
||||
let colour_before = modulation(&plain, SIZE, 4, chroma_r);
|
||||
let colour_after = modulation(&chroma, SIZE, 4, chroma_r);
|
||||
@@ -338,7 +344,13 @@ fn luminance_noise_reduction_never_moves_colour() {
|
||||
let mut off = AdjustPass::new(&ctx);
|
||||
let plain = render(&ctx, &mut off, &graph_with(0.0, 0.0), &source, (SIZE, SIZE));
|
||||
let mut on = AdjustPass::new(&ctx);
|
||||
let luma = render(&ctx, &mut on, &graph_with(100.0, 0.0), &source, (SIZE, SIZE));
|
||||
let luma = render(
|
||||
&ctx,
|
||||
&mut on,
|
||||
&graph_with(100.0, 0.0),
|
||||
&source,
|
||||
(SIZE, SIZE),
|
||||
);
|
||||
|
||||
// The colour difference — not the raw channel, which follows lightness.
|
||||
let row = SIZE / 2;
|
||||
@@ -430,14 +442,26 @@ fn the_same_edit_denoises_the_same_at_two_resolutions() {
|
||||
|
||||
let proxy_size = SOURCE / 2;
|
||||
let mut proxy_pass = AdjustPass::new(&ctx);
|
||||
let proxy = render(&ctx, &mut proxy_pass, &graph, &source, (proxy_size, proxy_size));
|
||||
let proxy = render(
|
||||
&ctx,
|
||||
&mut proxy_pass,
|
||||
&graph,
|
||||
&source,
|
||||
(proxy_size, proxy_size),
|
||||
);
|
||||
let proxy_amp = modulation(&proxy, proxy_size, HALF_PERIOD / 2, chroma_r);
|
||||
|
||||
// Both must be doing something: two flat images would agree perfectly and
|
||||
// prove nothing. About 0.0098 of 0.0188, by the derivation above.
|
||||
let untouched = {
|
||||
let mut pass = AdjustPass::new(&ctx);
|
||||
let plain = render(&ctx, &mut pass, &graph_with(0.0, 0.0), &source, (SOURCE, SOURCE));
|
||||
let plain = render(
|
||||
&ctx,
|
||||
&mut pass,
|
||||
&graph_with(0.0, 0.0),
|
||||
&source,
|
||||
(SOURCE, SOURCE),
|
||||
);
|
||||
modulation(&plain, SOURCE, HALF_PERIOD, chroma_r)
|
||||
};
|
||||
assert!(
|
||||
@@ -486,7 +510,13 @@ fn each_amount_costs_only_the_dispatches_it_needs() {
|
||||
|
||||
for (luminance, chroma, expected) in [(60.0, 0.0, 1), (0.0, 60.0, 2), (60.0, 60.0, 3)] {
|
||||
let mut pass = AdjustPass::new(&ctx);
|
||||
render(&ctx, &mut pass, &graph_with(luminance, chroma), &source, (SIZE, SIZE));
|
||||
render(
|
||||
&ctx,
|
||||
&mut pass,
|
||||
&graph_with(luminance, chroma),
|
||||
&source,
|
||||
(SIZE, SIZE),
|
||||
);
|
||||
assert_eq!(
|
||||
pass.detail_dispatches(),
|
||||
expected,
|
||||
@@ -496,7 +526,13 @@ fn each_amount_costs_only_the_dispatches_it_needs() {
|
||||
}
|
||||
|
||||
let mut neutral = AdjustPass::new(&ctx);
|
||||
let a = render(&ctx, &mut neutral, &graph_with(0.0, 0.0), &source, (SIZE, SIZE));
|
||||
let a = render(
|
||||
&ctx,
|
||||
&mut neutral,
|
||||
&graph_with(0.0, 0.0),
|
||||
&source,
|
||||
(SIZE, SIZE),
|
||||
);
|
||||
assert_eq!(neutral.detail_dispatches(), 0);
|
||||
assert_eq!(neutral.detail_allocations(), 0, "nothing was allocated");
|
||||
|
||||
@@ -504,7 +540,13 @@ fn each_amount_costs_only_the_dispatches_it_needs() {
|
||||
// touch the image, and a stage that ran and wrote back the same values
|
||||
// would still have quantised twice.
|
||||
let mut absent = AdjustPass::new(&ctx);
|
||||
let b = render(&ctx, &mut absent, &EditGraph::default_chain(), &source, (SIZE, SIZE));
|
||||
let b = render(
|
||||
&ctx,
|
||||
&mut absent,
|
||||
&EditGraph::default_chain(),
|
||||
&source,
|
||||
(SIZE, SIZE),
|
||||
);
|
||||
assert_eq!(a, b, "a neutral denoiser changed the picture");
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,9 @@ fn ctx() -> Option<GpuContext> {
|
||||
/// The centre pixel's red, green and blue, after `graph` has run over flat
|
||||
/// mid-grey.
|
||||
fn rendered(ctx: &GpuContext, graph: &EditGraph) -> (u8, u8, u8) {
|
||||
let data: Vec<u8> = (0..SIZE * SIZE).flat_map(|_| [128, 128, 128, 255]).collect();
|
||||
let data: Vec<u8> = (0..SIZE * SIZE)
|
||||
.flat_map(|_| [128, 128, 128, 255])
|
||||
.collect();
|
||||
let source = DemosaicedImage::from_rgba8(ctx, &data, SIZE, SIZE).expect("upload");
|
||||
|
||||
// Composed the way the display path composes it. A curve that generates
|
||||
@@ -44,11 +46,7 @@ fn rendered(ctx: &GpuContext, graph: &EditGraph) -> (u8, u8, u8) {
|
||||
/// an edit.
|
||||
fn lifted(channel: Channel) -> EditGraph {
|
||||
let mut graph = EditGraph::default_chain();
|
||||
graph.set_param(
|
||||
curve::ID,
|
||||
curve::coordinate(channel, 2, Axis::Y),
|
||||
0.75,
|
||||
);
|
||||
graph.set_param(curve::ID, curve::coordinate(channel, 2, Axis::Y), 0.75);
|
||||
graph
|
||||
}
|
||||
|
||||
@@ -62,7 +60,10 @@ fn the_master_curve_lifts_every_component_together() {
|
||||
let (r0, g0, b0) = rendered(&ctx, &EditGraph::default_chain());
|
||||
let (r, g, b) = rendered(&ctx, &lifted(Channel::Master));
|
||||
|
||||
assert!(r > r0, "the master curve did not lift the image: {r} vs {r0}");
|
||||
assert!(
|
||||
r > r0,
|
||||
"the master curve did not lift the image: {r} vs {r0}"
|
||||
);
|
||||
// Grey in, grey out: the master curve is applied as a ratio over
|
||||
// luminance, so it changes tone and not hue. A tolerance of one code
|
||||
// value, because the components travel through the ratio separately and
|
||||
|
||||
@@ -1532,7 +1532,10 @@ fn emit_node(out: &mut String, node: &Node) -> Result<(), String> {
|
||||
.iter()
|
||||
.map(|a| {
|
||||
let mut c = a.chars();
|
||||
let head = c.next().expect("attribute names are non-empty").to_uppercase();
|
||||
let head = c
|
||||
.next()
|
||||
.expect("attribute names are non-empty")
|
||||
.to_uppercase();
|
||||
format!("Attribute::{head}{}", c.as_str())
|
||||
})
|
||||
.collect();
|
||||
|
||||
@@ -499,11 +499,7 @@ pub fn compose_detail(
|
||||
// case where a photographer has a kernel switched on at a scale that
|
||||
// cannot draw it — against the alternative of the preview failing outright
|
||||
// or `compose_full` growing a resolution argument it has no other use for.
|
||||
if planned.is_empty()
|
||||
&& ops
|
||||
.iter()
|
||||
.any(|o| o.is_active() && o.detail().is_some())
|
||||
{
|
||||
if planned.is_empty() && ops.iter().any(|o| o.is_active() && o.detail().is_some()) {
|
||||
return ComposedDetail {
|
||||
passes: vec![compose_one(
|
||||
RESOLVE_ID,
|
||||
|
||||
@@ -8,8 +8,7 @@
|
||||
//! so reordering the pipeline needs no code change.
|
||||
|
||||
use crate::descriptor::{
|
||||
Attribute,
|
||||
Facet, LocalizedKey, OpDescriptor, OpId, ParamId, ParamKind, Presentation,
|
||||
Attribute, Facet, LocalizedKey, OpDescriptor, OpId, ParamId, ParamKind, Presentation,
|
||||
};
|
||||
use crate::framing::{CropRect, Framing};
|
||||
use crate::mask::MaskStack;
|
||||
@@ -378,7 +377,11 @@ impl EditGraph {
|
||||
/// Only the detail stage needs this. Every point operation is scale-free
|
||||
/// — a multiply is a multiply at any resolution — which is why nothing in
|
||||
/// the pipeline had to know its own size until a kernel arrived.
|
||||
pub fn render_scale(&self, source: (u32, u32), render: (u32, u32)) -> crate::detail::RenderScale {
|
||||
pub fn render_scale(
|
||||
&self,
|
||||
source: (u32, u32),
|
||||
render: (u32, u32),
|
||||
) -> crate::detail::RenderScale {
|
||||
let (fw, fh) = self.framing.output_size(source.0, source.1);
|
||||
let view = self.framing.view();
|
||||
// The *viewed* part of the framed image, at source resolution. Zoom
|
||||
@@ -397,7 +400,10 @@ impl EditGraph {
|
||||
/// Empty for every edit with no active neighbourhood operation, which is
|
||||
/// almost all of them — and in that case [`Self::compose`] emits the
|
||||
/// single encoded dispatch it always has.
|
||||
pub fn compose_detail(&self, scale: crate::detail::RenderScale) -> crate::detail::ComposedDetail {
|
||||
pub fn compose_detail(
|
||||
&self,
|
||||
scale: crate::detail::RenderScale,
|
||||
) -> crate::detail::ComposedDetail {
|
||||
self.compose_detail_for(scale, dr_types::ColourSpace::Srgb)
|
||||
}
|
||||
|
||||
|
||||
@@ -44,8 +44,8 @@ pub mod preset;
|
||||
pub mod sidecar;
|
||||
|
||||
pub use descriptor::{
|
||||
Attribute, Facet, LocalizedKey, OpDescriptor, OpId, ParamDescriptor, ParamId, ParamKind, Presentation,
|
||||
Scale, Unit, WidgetDemand, WidgetKind,
|
||||
Attribute, Facet, LocalizedKey, OpDescriptor, OpId, ParamDescriptor, ParamId, ParamKind,
|
||||
Presentation, Scale, Unit, WidgetDemand, WidgetKind,
|
||||
};
|
||||
pub use detail::{
|
||||
compose_detail, ComposedDetail, ComposedDetailPass, DetailPass, DetailStage, RenderScale,
|
||||
|
||||
@@ -856,7 +856,11 @@ impl MaskLayer {
|
||||
pub fn begin_stroke(&mut self, erase: bool, radius: f32, hardness: f32, flow: f32) -> bool {
|
||||
let full = self.stroke_points() >= MAX_LAYER_POINTS;
|
||||
let MaskSource::Brush { strokes } = &mut self.source else {
|
||||
log::warn!("layer {} is a {} mask, not a brush", self.id, self.source.kind());
|
||||
log::warn!(
|
||||
"layer {} is a {} mask, not a brush",
|
||||
self.id,
|
||||
self.source.kind()
|
||||
);
|
||||
return false;
|
||||
};
|
||||
if full {
|
||||
@@ -1121,7 +1125,10 @@ impl MaskStack {
|
||||
|
||||
/// Generate a fresh layer id that does not collide with an existing one.
|
||||
pub fn next_id(&self) -> String {
|
||||
(1..).map(|n| format!("m{n}")).find(|id| self.get(id).is_none()).expect("infinite range")
|
||||
(1..)
|
||||
.map(|n| format!("m{n}"))
|
||||
.find(|id| self.get(id).is_none())
|
||||
.expect("infinite range")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1178,10 +1185,7 @@ pub(crate) fn compose_layers(stack: &MaskStack) -> LayerShader {
|
||||
// place. A second copy here would be a second thing to keep in step
|
||||
// with `Framing::wgsl_prologue`, and the failure would be a mask that
|
||||
// is subtly wrong only when straightened.
|
||||
let _ = writeln!(
|
||||
out.body,
|
||||
" var m = sample_mask(uv_src, {slot});"
|
||||
);
|
||||
let _ = writeln!(out.body, " var m = sample_mask(uv_src, {slot});");
|
||||
let _ = writeln!(
|
||||
out.body,
|
||||
" m = select(m, 1.0 - m, u.{prefix}_invert > 0.5);"
|
||||
@@ -1403,7 +1407,11 @@ mod tests {
|
||||
assert!(shader.uniform_fields.contains("mask1_exposure_"));
|
||||
assert_eq!(
|
||||
shader.uniform_values.len(),
|
||||
shader.uniform_fields.lines().filter(|l| l.trim_start().starts_with("mask")).count(),
|
||||
shader
|
||||
.uniform_fields
|
||||
.lines()
|
||||
.filter(|l| l.trim_start().starts_with("mask"))
|
||||
.count(),
|
||||
"one value per emitted field"
|
||||
);
|
||||
}
|
||||
@@ -1448,7 +1456,11 @@ mod tests {
|
||||
// A gradient has no region ids, so nothing can go stale about it.
|
||||
let grad = MaskLayer::new(
|
||||
"m2",
|
||||
MaskSource::Linear { centre: (0.5, 0.5), angle: 0.0, width: 0.2 },
|
||||
MaskSource::Linear {
|
||||
centre: (0.5, 0.5),
|
||||
angle: 0.0,
|
||||
width: 0.2,
|
||||
},
|
||||
);
|
||||
assert!(!grad.is_stale(999));
|
||||
}
|
||||
@@ -1458,8 +1470,16 @@ mod tests {
|
||||
let base = segmentation_signature(1600, 1067, 6730, 2);
|
||||
assert_eq!(base, segmentation_signature(1600, 1067, 6730, 2));
|
||||
assert_ne!(base, segmentation_signature(1600, 1067, 6730, 5), "tuning");
|
||||
assert_ne!(base, segmentation_signature(800, 1067, 6730, 2), "proxy size");
|
||||
assert_ne!(base, segmentation_signature(1600, 1067, 42, 2), "region count");
|
||||
assert_ne!(
|
||||
base,
|
||||
segmentation_signature(800, 1067, 6730, 2),
|
||||
"proxy size"
|
||||
);
|
||||
assert_ne!(
|
||||
base,
|
||||
segmentation_signature(1600, 1067, 42, 2),
|
||||
"region count"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -472,10 +472,7 @@ pub fn compose_full(
|
||||
// than from a flag the caller sets, because a caller that got the flag
|
||||
// wrong would produce a shader whose storage format does not match the
|
||||
// texture bound to it.
|
||||
let output_mode = if ops
|
||||
.iter()
|
||||
.any(|o| o.is_active() && o.detail().is_some())
|
||||
{
|
||||
let output_mode = if ops.iter().any(|o| o.is_active() && o.detail().is_some()) {
|
||||
OutputMode::LinearWorking
|
||||
} else {
|
||||
OutputMode::Encoded
|
||||
@@ -1389,7 +1386,10 @@ mod tests {
|
||||
// how a shoulder rolls off.
|
||||
let mut curve = crate::ops::ToneCurve::new();
|
||||
curve.set_param(crate::ops::curve::P2_Y, 0.7);
|
||||
assert!(curve.is_active(), "the fixture must actually reach the shader");
|
||||
assert!(
|
||||
curve.is_active(),
|
||||
"the fixture must actually reach the shader"
|
||||
);
|
||||
|
||||
let source = compose(&[Box::new(curve)]).source;
|
||||
assert_eq!(
|
||||
|
||||
@@ -32,7 +32,9 @@
|
||||
//! least to perceived sharpness. Scaling all three about a virtual reference
|
||||
//! would soften the image even when the correction is right.
|
||||
|
||||
use crate::descriptor::{Attribute, LocalizedKey, OpDescriptor, OpId, ParamDescriptor, ParamId, Scale, Unit};
|
||||
use crate::descriptor::{
|
||||
Attribute, LocalizedKey, OpDescriptor, OpId, ParamDescriptor, ParamId, Scale, Unit,
|
||||
};
|
||||
use crate::lens::Warp;
|
||||
use crate::operation::{Helper, Uniform};
|
||||
|
||||
|
||||
@@ -764,9 +764,11 @@ mod tests {
|
||||
};
|
||||
assert_eq!(gate_of(0.0), 0.0);
|
||||
assert!((gate_of(1.0) - 0.25).abs() < 1e-6);
|
||||
assert!(chain_at(&sharpening(50.0, 1.0), RenderScale::full((512, 512))).passes[0]
|
||||
assert!(
|
||||
chain_at(&sharpening(50.0, 1.0), RenderScale::full((512, 512))).passes[0]
|
||||
.source
|
||||
.contains("gate > 0.0"));
|
||||
.contains("gate > 0.0")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -783,7 +785,11 @@ mod tests {
|
||||
for scale in scales {
|
||||
for pass in chain_at(&sharpening(75.0, 1.0), scale).passes {
|
||||
assert_eq!(pass.uniforms.len() % 4, 0, "{}", pass.label);
|
||||
assert!(pass.uniforms.iter().all(|v| v.is_finite()), "{}", pass.label);
|
||||
assert!(
|
||||
pass.uniforms.iter().all(|v| v.is_finite()),
|
||||
"{}",
|
||||
pass.label
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -805,10 +811,38 @@ mod tests {
|
||||
// word be safe in one stage and not in the other, which is the sort of
|
||||
// difference nobody discovers until a shader fails to compile.
|
||||
const RESERVED: &[&str] = &[
|
||||
"target", "sample", "filter", "texture", "buffer", "binding", "const", "enum", "mat",
|
||||
"vec", "ptr", "ref", "shared", "static", "typedef", "union", "unless", "handle",
|
||||
"layout", "packed", "premerge", "regardless", "active", "do", "input", "output",
|
||||
"private", "resource", "restrict", "self", "std", "where",
|
||||
"target",
|
||||
"sample",
|
||||
"filter",
|
||||
"texture",
|
||||
"buffer",
|
||||
"binding",
|
||||
"const",
|
||||
"enum",
|
||||
"mat",
|
||||
"vec",
|
||||
"ptr",
|
||||
"ref",
|
||||
"shared",
|
||||
"static",
|
||||
"typedef",
|
||||
"union",
|
||||
"unless",
|
||||
"handle",
|
||||
"layout",
|
||||
"packed",
|
||||
"premerge",
|
||||
"regardless",
|
||||
"active",
|
||||
"do",
|
||||
"input",
|
||||
"output",
|
||||
"private",
|
||||
"resource",
|
||||
"restrict",
|
||||
"self",
|
||||
"std",
|
||||
"where",
|
||||
];
|
||||
|
||||
for pass in chain_at(&sharpening(100.0, 2.0), RenderScale::full((512, 512))).passes {
|
||||
|
||||
@@ -30,7 +30,9 @@
|
||||
//! adjusted, each one's share depends on what the other is set to, so turning
|
||||
//! up one colour's saturation quietly weakened its neighbour's hue shift.
|
||||
|
||||
use crate::descriptor::{Attribute, Facet, LocalizedKey, OpDescriptor, OpId, ParamDescriptor, ParamId};
|
||||
use crate::descriptor::{
|
||||
Attribute, Facet, LocalizedKey, OpDescriptor, OpId, ParamDescriptor, ParamId,
|
||||
};
|
||||
use crate::operation::{Helper, Operation, Uniform};
|
||||
use crate::ops::helpers;
|
||||
|
||||
|
||||
@@ -121,12 +121,7 @@ impl Channel {
|
||||
/// Master first because it runs first — see the module documentation for
|
||||
/// why that is the composition order — and because a list showing the
|
||||
/// grade before the tone would be describing a different operation.
|
||||
pub const ALL: [Channel; 4] = [
|
||||
Channel::Master,
|
||||
Channel::Red,
|
||||
Channel::Green,
|
||||
Channel::Blue,
|
||||
];
|
||||
pub const ALL: [Channel; 4] = [Channel::Master, Channel::Red, Channel::Green, Channel::Blue];
|
||||
|
||||
/// Position in [`Self::ALL`], and so in every array keyed by channel.
|
||||
pub const fn index(self) -> usize {
|
||||
@@ -771,7 +766,10 @@ impl Operation for ToneCurve {
|
||||
// declared from, so a call and its uniform block cannot disagree
|
||||
// about a name.
|
||||
let args = UNIFORM_NAMES[channel.index()].join(", ");
|
||||
let _ = writeln!(body, "c.{component} = channel_curve(c.{component}, {args});");
|
||||
let _ = writeln!(
|
||||
body,
|
||||
"c.{component} = channel_curve(c.{component}, {args});"
|
||||
);
|
||||
}
|
||||
|
||||
// Whichever curves ran, the result has to be a colour: the spline's
|
||||
@@ -1345,7 +1343,9 @@ mod tests {
|
||||
|
||||
let body = c.wgsl_body();
|
||||
let master = body.find("apply_tone_gain").expect("the master curve runs");
|
||||
let red = body.find("c.r = channel_curve").expect("the red curve runs");
|
||||
let red = body
|
||||
.find("c.r = channel_curve")
|
||||
.expect("the red curve runs");
|
||||
assert!(master < red, "the master curve must run first:\n{body}");
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,9 @@
|
||||
//! profile is a "make the horizon straight" task, which one term does well.
|
||||
//! The full triple is reachable by loading a profile.
|
||||
|
||||
use crate::descriptor::{Attribute, LocalizedKey, OpDescriptor, OpId, ParamDescriptor, ParamId, Scale, Unit};
|
||||
use crate::descriptor::{
|
||||
Attribute, LocalizedKey, OpDescriptor, OpId, ParamDescriptor, ParamId, Scale, Unit,
|
||||
};
|
||||
use crate::lens::Warp;
|
||||
use crate::operation::{Helper, Uniform};
|
||||
|
||||
|
||||
@@ -619,7 +619,10 @@ mod tests {
|
||||
let scale = RenderScale::full((2000, 1500));
|
||||
let only_clarity = composed(50.0, 0.0, scale);
|
||||
assert_eq!(only_clarity.len(), 2, "one operation, two passes");
|
||||
assert!(only_clarity.passes.iter().all(|p| p.label.starts_with("clarity/")));
|
||||
assert!(only_clarity
|
||||
.passes
|
||||
.iter()
|
||||
.all(|p| p.label.starts_with("clarity/")));
|
||||
|
||||
let both = composed(50.0, 50.0, scale);
|
||||
assert_eq!(both.len(), 4);
|
||||
@@ -701,7 +704,10 @@ mod tests {
|
||||
// acutance family's unit.
|
||||
let proxy = RenderScale::new((2000, 1500), (6000, 4500));
|
||||
let clarity = Clarity::with_amount(60.0);
|
||||
assert_eq!(clarity.kernel(proxy), clarity.kernel(RenderScale::full((2000, 1500))));
|
||||
assert_eq!(
|
||||
clarity.kernel(proxy),
|
||||
clarity.kernel(RenderScale::full((2000, 1500)))
|
||||
);
|
||||
assert!(
|
||||
proxy.source_pixels(96.0) < 40.0,
|
||||
"the same length in the other unit would have collapsed"
|
||||
@@ -786,7 +792,9 @@ mod tests {
|
||||
// slider can produce is `gain * threshold` stops however violent the
|
||||
// edge — a bound that holds by construction rather than by tuning.
|
||||
let combine = &Clarity::with_amount(100.0).passes(RenderScale::full((2000, 1500)))[1];
|
||||
assert!(combine.wgsl.contains("threshold * tanh(detail / threshold)"));
|
||||
assert!(combine
|
||||
.wgsl
|
||||
.contains("threshold * tanh(detail / threshold)"));
|
||||
|
||||
let bound = |u: &[Uniform]| {
|
||||
let get = |n| u.iter().find(|x| x.name == n).unwrap().value;
|
||||
@@ -819,7 +827,10 @@ mod tests {
|
||||
|
||||
// And texture does not carry the helpers it would need for one, so its
|
||||
// shader says what it does rather than merely not calling it.
|
||||
assert!(Texture::new().helpers().iter().any(|h| h.name == "log_luma"));
|
||||
assert!(Texture::new()
|
||||
.helpers()
|
||||
.iter()
|
||||
.any(|h| h.name == "log_luma"));
|
||||
assert!(!Texture::new()
|
||||
.helpers()
|
||||
.iter()
|
||||
|
||||
@@ -820,7 +820,13 @@ mod tests {
|
||||
// Everything else about the two must match, or one axis is filtered
|
||||
// harder than the other and a round blotch comes out oval.
|
||||
assert_eq!(passes[0].radius, passes[1].radius);
|
||||
for name in ["radius", "inv_spatial", "guide_k", "chroma_k", "noise_floor"] {
|
||||
for name in [
|
||||
"radius",
|
||||
"inv_spatial",
|
||||
"guide_k",
|
||||
"chroma_k",
|
||||
"noise_floor",
|
||||
] {
|
||||
let of = |p: &DetailPass| {
|
||||
p.uniforms
|
||||
.iter()
|
||||
|
||||
@@ -67,9 +67,7 @@ use std::fmt;
|
||||
use std::fmt::Write as _;
|
||||
|
||||
use crate::graph::EditGraph;
|
||||
use crate::mask::{
|
||||
Falloff, MaskLayer, MaskSource, MaskStack, Morphology, Stroke, DEFAULT_FEATHER,
|
||||
};
|
||||
use crate::mask::{Falloff, MaskLayer, MaskSource, MaskStack, Morphology, Stroke, DEFAULT_FEATHER};
|
||||
use crate::preset::{resolve, Preset};
|
||||
|
||||
/// Format version of the document itself.
|
||||
@@ -662,7 +660,6 @@ impl Sidecar {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Write one mask layer as its own block.
|
||||
///
|
||||
/// The version uuid is repeated in the header rather than relying on the
|
||||
@@ -1292,7 +1289,11 @@ mod tests {
|
||||
// else.
|
||||
assert_eq!(g.param(curve::ID, curve::P1_Y), Some(0.15));
|
||||
assert_eq!(g.param(curve::ID, curve::P3_Y), Some(0.85));
|
||||
for channel in [curve::Channel::Red, curve::Channel::Green, curve::Channel::Blue] {
|
||||
for channel in [
|
||||
curve::Channel::Red,
|
||||
curve::Channel::Green,
|
||||
curve::Channel::Blue,
|
||||
] {
|
||||
for point in 0..curve::POINTS {
|
||||
for axis in [curve::Axis::X, curve::Axis::Y] {
|
||||
let id = curve::coordinate(channel, point, axis);
|
||||
|
||||
@@ -40,7 +40,10 @@ fn the_groups_are_derivable_from_the_chain() {
|
||||
present.sort();
|
||||
present.dedup();
|
||||
|
||||
assert!(present.contains(&Attribute::Tone), "the chain has tonal work");
|
||||
assert!(
|
||||
present.contains(&Attribute::Tone),
|
||||
"the chain has tonal work"
|
||||
);
|
||||
assert!(present.contains(&Attribute::Colour));
|
||||
assert!(
|
||||
present.contains(&Attribute::Geometry),
|
||||
|
||||
@@ -55,7 +55,14 @@ fn a_region_mask_survives_a_round_trip() {
|
||||
assert_eq!(layer.id, "m1");
|
||||
assert_eq!(layer.name, "Subject");
|
||||
assert_eq!(layer.source, regions(&[3, 7, 12]));
|
||||
assert_eq!(layer.ops.iter().find(|o| o.descriptor().id.0 == "exposure").map(|o| o.param(ParamId("exposure"))), Some(0.75));
|
||||
assert_eq!(
|
||||
layer
|
||||
.ops
|
||||
.iter()
|
||||
.find(|o| o.descriptor().id.0 == "exposure")
|
||||
.map(|o| o.param(ParamId("exposure"))),
|
||||
Some(0.75)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -120,7 +127,10 @@ fn flags_and_opacity_survive() {
|
||||
let layer = &restored.masks().layers()[0];
|
||||
assert!(layer.invert);
|
||||
assert!((layer.opacity - 0.35).abs() < 1e-6);
|
||||
assert!(!layer.enabled, "a disabled layer must stay disabled, not vanish");
|
||||
assert!(
|
||||
!layer.enabled,
|
||||
"a disabled layer must stay disabled, not vanish"
|
||||
);
|
||||
}
|
||||
|
||||
/// A selection's identity is the *set*. Two files naming the same regions in
|
||||
@@ -272,7 +282,10 @@ fn unknown_top_level_keys_still_round_trip_alongside_masks() {
|
||||
exposure.exposure = 1\n";
|
||||
|
||||
let out = Sidecar::parse(text).expect("parse").to_text();
|
||||
assert!(out.contains("future.thing"), "unknown keys are still preserved");
|
||||
assert!(
|
||||
out.contains("future.thing"),
|
||||
"unknown keys are still preserved"
|
||||
);
|
||||
assert!(out.contains("[mask default m1]"));
|
||||
}
|
||||
|
||||
@@ -361,7 +374,13 @@ fn a_brush_layer_writes_one_line_per_stroke() {
|
||||
.lines()
|
||||
.filter(|l| l.starts_with("stroke = "))
|
||||
.collect();
|
||||
assert_eq!(lines, ["stroke = add 0.05 0.5 1 0.2,0.5 0.8,0.5", "stroke = erase 0.05 0.5 1 0.5,0.5"]);
|
||||
assert_eq!(
|
||||
lines,
|
||||
[
|
||||
"stroke = add 0.05 0.5 1 0.2,0.5 0.8,0.5",
|
||||
"stroke = erase 0.05 0.5 1 0.5,0.5"
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -12,7 +12,8 @@ use dr_pipeline::{EditGraph, Sidecar};
|
||||
|
||||
/// A version block carrying `params`, in the on-disk spelling.
|
||||
fn sidecar_with(params: &[(&str, f32)]) -> String {
|
||||
let mut text = String::from("drsc 1\n\n[version u1]\nname = Default\nrevision = 2\nmodified = 0\n");
|
||||
let mut text =
|
||||
String::from("drsc 1\n\n[version u1]\nname = Default\nrevision = 2\nmodified = 0\n");
|
||||
for (key, value) in params {
|
||||
text.push_str(&format!("{key} = {value}\n"));
|
||||
}
|
||||
@@ -199,4 +200,3 @@ fn every_point_of_every_curve_round_trips_through_a_sidecar() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -46,7 +46,11 @@ fn main() {
|
||||
};
|
||||
println!(
|
||||
"tiling {}",
|
||||
if tiled { "grid, 25% overlap" } else { "whole frame" }
|
||||
if tiled {
|
||||
"grid, 25% overlap"
|
||||
} else {
|
||||
"whole frame"
|
||||
}
|
||||
);
|
||||
|
||||
let t0 = std::time::Instant::now();
|
||||
@@ -57,10 +61,7 @@ fn main() {
|
||||
let instances = model
|
||||
.detect(&rgb, width, height, &options)
|
||||
.expect("inference");
|
||||
println!(
|
||||
"detect {:.0} ms",
|
||||
t1.elapsed().as_secs_f32() * 1000.0
|
||||
);
|
||||
println!("detect {:.0} ms", t1.elapsed().as_secs_f32() * 1000.0);
|
||||
println!("found {} instances", instances.len());
|
||||
|
||||
for (i, inst) in instances.iter().enumerate() {
|
||||
@@ -131,10 +132,7 @@ fn load_jpeg(path: &str) -> (Vec<f32>, usize, usize) {
|
||||
// where *not* linearising is the correct thing to do.
|
||||
let rgb = match pixels.len() / (w * h) {
|
||||
3 => pixels.iter().map(|&v| v as f32 / 255.0).collect(),
|
||||
1 => pixels
|
||||
.iter()
|
||||
.flat_map(|&v| [v as f32 / 255.0; 3])
|
||||
.collect(),
|
||||
1 => pixels.iter().flat_map(|&v| [v as f32 / 255.0; 3]).collect(),
|
||||
n => panic!("unexpected {n} components per pixel"),
|
||||
};
|
||||
|
||||
|
||||
@@ -346,8 +346,8 @@ impl Shaped {
|
||||
radius_px: f32,
|
||||
) -> Self {
|
||||
let distance = signed_distance(coverage, width, height, threshold);
|
||||
let distance = apply_morphology(&distance, width, height, morphology, radius_px)
|
||||
.unwrap_or(distance);
|
||||
let distance =
|
||||
apply_morphology(&distance, width, height, morphology, radius_px).unwrap_or(distance);
|
||||
Self {
|
||||
distance,
|
||||
width,
|
||||
@@ -403,7 +403,11 @@ mod tests {
|
||||
// Distance from the boundary, so the half-pixel comes back off to
|
||||
// compare against the centre-to-centre figures.
|
||||
let at = |x: usize, y: usize| -d[y * 5 + x] + 0.5;
|
||||
assert!((at(1, 1) - std::f32::consts::SQRT_2).abs() < 1e-4, "{}", at(1, 1));
|
||||
assert!(
|
||||
(at(1, 1) - std::f32::consts::SQRT_2).abs() < 1e-4,
|
||||
"{}",
|
||||
at(1, 1)
|
||||
);
|
||||
assert!((at(0, 0) - (8.0f32).sqrt()).abs() < 1e-4, "{}", at(0, 0));
|
||||
assert_eq!(at(2, 0), 2.0, "straight up is exactly two");
|
||||
}
|
||||
@@ -489,7 +493,10 @@ mod tests {
|
||||
m[10 * w + 10] = 0;
|
||||
|
||||
let before = signed_distance(&m, w, h, 128);
|
||||
assert!(before[10 * w + 10] < 0.0, "the hole starts outside the mask");
|
||||
assert!(
|
||||
before[10 * w + 10] < 0.0,
|
||||
"the hole starts outside the mask"
|
||||
);
|
||||
|
||||
let after = apply_morphology(&before, w, h, Morphology::Close, 2.0).expect("recomputed");
|
||||
assert!(after[10 * w + 10] >= 0.0, "closing should have filled it");
|
||||
|
||||
@@ -142,7 +142,11 @@ impl Membership {
|
||||
|
||||
/// The instance a region belongs to, if any.
|
||||
pub fn instance_of(&self, region: u32) -> Option<usize> {
|
||||
self.of.get(region as usize).copied().flatten().map(|(i, _)| i)
|
||||
self.of
|
||||
.get(region as usize)
|
||||
.copied()
|
||||
.flatten()
|
||||
.map(|(i, _)| i)
|
||||
}
|
||||
|
||||
/// How two regions relate semantically, in `-1.0..=1.0`.
|
||||
@@ -233,11 +237,7 @@ pub fn apply_semantic_prior(
|
||||
///
|
||||
/// The returned ids are sorted, so the same click always produces the same
|
||||
/// mask — which is what lets it be a cache key.
|
||||
pub fn regions_for_instance(
|
||||
field: &RegionField,
|
||||
mask: &[f32],
|
||||
options: &PriorOptions,
|
||||
) -> Vec<u32> {
|
||||
pub fn regions_for_instance(field: &RegionField, mask: &[f32], options: &PriorOptions) -> Vec<u32> {
|
||||
let mut coverage = vec![0.0f32; field.region_count];
|
||||
let mut sizes = vec![0u32; field.region_count];
|
||||
|
||||
@@ -278,10 +278,26 @@ mod tests {
|
||||
labels: vec![0, 0, 2, 2, 1, 1, 3, 3],
|
||||
region_count: 4,
|
||||
adjacency: vec![
|
||||
Edge { a: 0, b: 1, saddle: 1.0 },
|
||||
Edge { a: 0, b: 2, saddle: 1.0 },
|
||||
Edge { a: 1, b: 3, saddle: 1.0 },
|
||||
Edge { a: 2, b: 3, saddle: 1.0 },
|
||||
Edge {
|
||||
a: 0,
|
||||
b: 1,
|
||||
saddle: 1.0,
|
||||
},
|
||||
Edge {
|
||||
a: 0,
|
||||
b: 2,
|
||||
saddle: 1.0,
|
||||
},
|
||||
Edge {
|
||||
a: 1,
|
||||
b: 3,
|
||||
saddle: 1.0,
|
||||
},
|
||||
Edge {
|
||||
a: 2,
|
||||
b: 3,
|
||||
saddle: 1.0,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
@@ -361,7 +377,10 @@ mod tests {
|
||||
#[test]
|
||||
fn zero_strength_is_arm_a_exactly() {
|
||||
let f = field();
|
||||
let opts = PriorOptions { strength: 0.0, ..PriorOptions::default() };
|
||||
let opts = PriorOptions {
|
||||
strength: 0.0,
|
||||
..PriorOptions::default()
|
||||
};
|
||||
let m = Membership::compute(&f, &[&left_half()], &opts).unwrap();
|
||||
assert_eq!(apply_semantic_prior(&f, &m, &opts).adjacency, f.adjacency);
|
||||
}
|
||||
@@ -394,7 +413,11 @@ mod tests {
|
||||
let err = Membership::compute(&f, &[&vec![0.0; 3]], &PriorOptions::default()).unwrap_err();
|
||||
assert_eq!(
|
||||
err,
|
||||
MembershipError::MaskSize { instance: 0, expected: 8, got: 3 }
|
||||
MembershipError::MaskSize {
|
||||
instance: 0,
|
||||
expected: 8,
|
||||
got: 3
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -403,11 +426,7 @@ mod tests {
|
||||
fn different_instances_repel() {
|
||||
let f = field();
|
||||
let right_half = vec![0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0];
|
||||
let m = Membership::compute(
|
||||
&f,
|
||||
&[&left_half(), &right_half],
|
||||
&PriorOptions::default(),
|
||||
)
|
||||
let m = Membership::compute(&f, &[&left_half(), &right_half], &PriorOptions::default())
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(m.instance_of(0), Some(0));
|
||||
|
||||
@@ -357,7 +357,6 @@ impl SemanticModel {
|
||||
options,
|
||||
))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// Turn one forward pass into instances in source-image space.
|
||||
@@ -543,7 +542,11 @@ fn assemble_mask(
|
||||
let (gx1, gy1) = ((gx0 + 1).min(pw - 1), (gy0 + 1).min(ph - 1));
|
||||
|
||||
let mut acc = 0.0;
|
||||
for (k, &c) in coeffs.iter().enumerate().take(PROTOTYPES.min(protos.shape()[0])) {
|
||||
for (k, &c) in coeffs
|
||||
.iter()
|
||||
.enumerate()
|
||||
.take(PROTOTYPES.min(protos.shape()[0]))
|
||||
{
|
||||
if c == 0.0 {
|
||||
continue;
|
||||
}
|
||||
@@ -643,7 +646,12 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn letterbox_round_trips_a_landscape_window() {
|
||||
let window = Window { x: 0.0, y: 0.0, w: 1600.0, h: 1067.0 };
|
||||
let window = Window {
|
||||
x: 0.0,
|
||||
y: 0.0,
|
||||
w: 1600.0,
|
||||
h: 1067.0,
|
||||
};
|
||||
let lb = Letterbox::fit(window.w, window.h);
|
||||
|
||||
// A source point maps into the input and back to where it started.
|
||||
|
||||
@@ -548,8 +548,10 @@ pub fn cancel_press(window: &AppWindow, ctl: &Rc<CollectionsController>, ids: &[
|
||||
let Some(undo) = ctl.press_undo.borrow_mut().take() else {
|
||||
return;
|
||||
};
|
||||
let (previous_anchor, cursor) =
|
||||
undo.restore(&mut ctl.selection.borrow_mut(), &mut ctl.anchor.borrow_mut());
|
||||
let (previous_anchor, cursor) = undo.restore(
|
||||
&mut ctl.selection.borrow_mut(),
|
||||
&mut ctl.anchor.borrow_mut(),
|
||||
);
|
||||
ctl.previous_anchor.set(previous_anchor);
|
||||
ctl.set_cursor(cursor);
|
||||
sync_selection(window, ctl, ids);
|
||||
|
||||
+33
-17
@@ -206,7 +206,13 @@ impl SegmentationJob {
|
||||
// few places in this codebase where not linearising is correct.
|
||||
let rgb = rgba
|
||||
.chunks_exact(4)
|
||||
.flat_map(|p| [p[0] as f32 / 255.0, p[1] as f32 / 255.0, p[2] as f32 / 255.0])
|
||||
.flat_map(|p| {
|
||||
[
|
||||
p[0] as f32 / 255.0,
|
||||
p[1] as f32 / 255.0,
|
||||
p[2] as f32 / 255.0,
|
||||
]
|
||||
})
|
||||
.collect();
|
||||
Ok((rgb, pw as usize, ph as usize))
|
||||
}
|
||||
@@ -1458,10 +1464,7 @@ impl DevelopSession {
|
||||
|
||||
/// What kind of mask a layer is — "regions", "linear", "radial".
|
||||
pub fn mask_kind(&self, id: &str) -> &'static str {
|
||||
self.graph
|
||||
.masks()
|
||||
.get(id)
|
||||
.map_or("", |l| l.source.kind())
|
||||
self.graph.masks().get(id).map_or("", |l| l.source.kind())
|
||||
}
|
||||
|
||||
pub fn mask_inverted(&self, id: &str) -> bool {
|
||||
@@ -1625,7 +1628,11 @@ impl DevelopSession {
|
||||
width: 0.3,
|
||||
}
|
||||
};
|
||||
if !self.graph.masks_mut().push(MaskLayer::new(id.clone(), source)) {
|
||||
if !self
|
||||
.graph
|
||||
.masks_mut()
|
||||
.push(MaskLayer::new(id.clone(), source))
|
||||
{
|
||||
return None;
|
||||
}
|
||||
self.active_mask = Some(id.clone());
|
||||
@@ -1660,7 +1667,8 @@ impl DevelopSession {
|
||||
pub fn set_mask_feather(&mut self, id: &str, feather: f32) {
|
||||
if let Some(layer) = self.graph.masks_mut().get_mut(id) {
|
||||
layer.feather = feather.clamp(0.0, 1.0);
|
||||
self.history.record(&self.graph, Edit::Op(OpId("mask-feather")));
|
||||
self.history
|
||||
.record(&self.graph, Edit::Op(OpId("mask-feather")));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1694,7 +1702,8 @@ impl DevelopSession {
|
||||
pub fn set_mask_morph_radius(&mut self, id: &str, radius: f32) {
|
||||
if let Some(layer) = self.graph.masks_mut().get_mut(id) {
|
||||
layer.morph_radius = radius.clamp(0.0, 1.0);
|
||||
self.history.record(&self.graph, Edit::Op(OpId("mask-morph")));
|
||||
self.history
|
||||
.record(&self.graph, Edit::Op(OpId("mask-morph")));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1702,7 +1711,10 @@ impl DevelopSession {
|
||||
pub fn mask_falloff(&self, id: &str) -> usize {
|
||||
use dr_pipeline::mask::Falloff;
|
||||
self.graph.masks().get(id).map_or(0, |l| {
|
||||
Falloff::ALL.iter().position(|&f| f == l.falloff).unwrap_or(0)
|
||||
Falloff::ALL
|
||||
.iter()
|
||||
.position(|&f| f == l.falloff)
|
||||
.unwrap_or(0)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1732,7 +1744,10 @@ impl DevelopSession {
|
||||
pub fn mask_is_shapeable(&self, id: &str) -> bool {
|
||||
use dr_pipeline::mask::MaskSource;
|
||||
self.graph.masks().get(id).is_some_and(|l| {
|
||||
matches!(l.source, MaskSource::Subject { .. } | MaskSource::Regions { .. })
|
||||
matches!(
|
||||
l.source,
|
||||
MaskSource::Subject { .. } | MaskSource::Regions { .. }
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1742,7 +1757,8 @@ impl DevelopSession {
|
||||
// `Op` rather than `Discrete`: opacity is dragged, and a drag is
|
||||
// one decision however many values it passes through. `Discrete`
|
||||
// would put every intermediate position on the undo stack.
|
||||
self.history.record(&self.graph, Edit::Op(OpId("mask-opacity")));
|
||||
self.history
|
||||
.record(&self.graph, Edit::Op(OpId("mask-opacity")));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2510,8 +2526,7 @@ mod tests {
|
||||
fn a_job_carries_the_session_it_was_taken_from() {
|
||||
let Some(ctx) = headless() else { return };
|
||||
let rgba: Vec<u8> = (0..16 * 16).flat_map(|_| [128, 128, 128, 255]).collect();
|
||||
let session =
|
||||
DevelopSession::open_rgb(&ctx, &rgba, 16, 16, dr_types::Orientation::NORMAL)
|
||||
let session = DevelopSession::open_rgb(&ctx, &rgba, 16, 16, dr_types::Orientation::NORMAL)
|
||||
.expect("session");
|
||||
assert_eq!(session.segmentation_job().session(), session.id());
|
||||
}
|
||||
@@ -2523,8 +2538,7 @@ mod tests {
|
||||
fn an_abandoned_job_does_no_work() {
|
||||
let Some(ctx) = headless() else { return };
|
||||
let rgba: Vec<u8> = (0..16 * 16).flat_map(|_| [128, 128, 128, 255]).collect();
|
||||
let session =
|
||||
DevelopSession::open_rgb(&ctx, &rgba, 16, 16, dr_types::Orientation::NORMAL)
|
||||
let session = DevelopSession::open_rgb(&ctx, &rgba, 16, 16, dr_types::Orientation::NORMAL)
|
||||
.expect("session");
|
||||
|
||||
let job = session.segmentation_job();
|
||||
@@ -3993,7 +4007,10 @@ mod tests {
|
||||
let narrowed = session.rows().len();
|
||||
|
||||
assert!(narrowed > 0, "a tab must show something");
|
||||
assert!(narrowed < all, "and less than everything: {narrowed} of {all}");
|
||||
assert!(
|
||||
narrowed < all,
|
||||
"and less than everything: {narrowed} of {all}"
|
||||
);
|
||||
}
|
||||
|
||||
/// The trap: `op_index` counts over *every* capability, so a row that
|
||||
@@ -4054,7 +4071,6 @@ mod tests {
|
||||
session.set_active_tab(99);
|
||||
assert_eq!(session.rows().len(), all);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// `dr_pipeline`'s morphology, as `dr_segment` names it.
|
||||
|
||||
@@ -844,7 +844,6 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/// The ordinary case: the answer comes back to the photograph that asked.
|
||||
#[test]
|
||||
fn a_result_for_the_open_photograph_is_applied() {
|
||||
|
||||
@@ -318,8 +318,8 @@ fn refresh_thumbnail_for(
|
||||
session: &Rc<RefCell<Option<DevelopSession>>>,
|
||||
library: &Rc<library_ui::LibraryController>,
|
||||
) {
|
||||
let worth_it = window.get_can_undo()
|
||||
|| session.borrow().as_ref().is_some_and(|s| !s.is_neutral());
|
||||
let worth_it =
|
||||
window.get_can_undo() || session.borrow().as_ref().is_some_and(|s| !s.is_neutral());
|
||||
if !worth_it {
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user