Projection chips redraw the preview; auto-crop as the DNG default crop

Picking a chip stored the choice for the merge and changed nothing on
screen — the chip did not even highlight, since the selected property
was never written back. Now the pick is reflected, and the job, waiting
for its decision, takes a Preview request, draws the alignment on the
chosen surface at proxy cost and reports again; the drain puts the new
picture and its size up. Auto is the surface the field of view suggests.

Also:
The largest rectangle inside the frames' coverage is found a row at a
time — a histogram of consecutive covered rows and a stack pass per row —
so the composite is never held to be measured (FR-MRG-11). It is written
as DefaultCropOrigin/DefaultCropSize (FR-MRG-4): the file opens on the
picture, the border is still in it, and resetting the crop shows it.
rawler reports the crop as the picture, which the test checks.

FR-MRG-4 records the question raised the same day — fill the border
rather than crop it — as open: a non-generative fill through the heal,
or a generative inpainter with its licence and weights. Neither decided.
This commit is contained in:
2026-09-19 15:24:20 +02:00
parent 30bd276d0b
commit 57ed51c1c5
7 changed files with 301 additions and 47 deletions
+35 -19
View File
@@ -110,6 +110,10 @@ impl MergeRequest {
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Decision {
Merge { projection: Option<Projection> },
/// Draw the alignment again on another surface, and report it again.
/// The page's projection chips: what a choice looks like before it is
/// confirmed, at proxy cost rather than the merge's.
Preview { projection: Option<Projection> },
Abandon,
}
@@ -279,8 +283,7 @@ fn run_inner(
let projection = request
.projection
.unwrap_or_else(|| Projection::suggest(hfov, vfov));
let bounds = projection::bounds(projection, focal_full, &cameras, frame_size)
.ok_or("the frames project nowhere")?;
let suggested = projection;
// 4. Gains, before the report so the preview shows them.
let gains = if alignment.is_complete() {
@@ -288,23 +291,25 @@ fn run_inner(
} else {
vec![1.0; frames.len()]
};
let preview = if alignment.is_complete() {
progress("Drawing the preview", 0, 1);
Some(preview(&colour, &proxies, &alignment, &gains, projection, &frames[0].raw))
} else {
None
let report = |projection: Projection| -> Result<AlignmentReport, String> {
let bounds = projection::bounds(projection, focal_full, &cameras, frame_size)
.ok_or("the frames project nowhere")?;
let preview = alignment
.is_complete()
.then(|| preview(&colour, &proxies, &alignment, &gains, projection, &frames[0].raw));
Ok(AlignmentReport {
focal_mm: focal_full * 36.0 / full_long,
rms_px: alignment.rms_px,
projection,
width: bounds.width().ceil() as u32,
height: bounds.height().ceil() as u32,
frames: describe(&alignment),
links: alignment.links.len(),
preview,
})
};
let report = AlignmentReport {
focal_mm: focal_full * 36.0 / full_long,
rms_px: alignment.rms_px,
projection,
width: bounds.width().ceil() as u32,
height: bounds.height().ceil() as u32,
frames: describe(&alignment),
links: alignment.links.len(),
preview,
};
let _ = events.send(MergeEvent::Aligned(report));
progress("Drawing the preview", 0, 1);
let _ = events.send(MergeEvent::Aligned(report(suggested)?));
if !alignment.is_complete() {
let names: Vec<String> = alignment
.unaligned
@@ -322,7 +327,10 @@ fn run_inner(
return Ok(None);
}
match decision.recv_timeout(std::time::Duration::from_millis(100)) {
Ok(Decision::Merge { projection: p }) => break p.unwrap_or(projection),
Ok(Decision::Merge { projection: p }) => break p.unwrap_or(suggested),
Ok(Decision::Preview { projection: p }) => {
let _ = events.send(MergeEvent::Aligned(report(p.unwrap_or(suggested))?));
}
Ok(Decision::Abandon) => return Ok(None),
Err(std::sync::mpsc::RecvTimeoutError::Timeout) => continue,
Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => return Ok(None),
@@ -388,6 +396,10 @@ fn run_inner(
// other's pace more than one band.
let (band_tx, band_rx) = std::sync::mpsc::sync_channel::<Vec<u16>>(1);
let rows_per_strip = request.chunk.1.max(1);
// The crop (FR-MRG-4), found as the bands go by; the writer reads it
// once the last strip is in, by which time every band has been seen.
let inscribed = Arc::new(std::sync::Mutex::new(dr_export::Inscribed::new(out_w)));
let inscribed_for_writer = inscribed.clone();
let file = std::fs::File::create(&out_path).map_err(|e| format!("{}: {e}", out_path.display()))?;
let writer = std::thread::spawn(move || -> Result<(), String> {
let mut file = std::io::BufWriter::new(file);
@@ -405,6 +417,7 @@ fn run_inner(
buf.extend_from_slice(&band);
Ok(())
},
|| inscribed_for_writer.lock().ok().map(|i| i.best()),
)
.map_err(|e| e.to_string())
});
@@ -435,6 +448,9 @@ fn run_inner(
|band| {
bands_done += 1;
progress("Merging", bands_done, total_bands);
if let Ok(mut i) = inscribed.lock() {
i.push_rows(band.covered, band.rows);
}
band_tx
.send(band.rgb.to_vec())
.map_err(|_| dr_gpu::GpuError::Readback("the writer stopped".into()))
+21 -6
View File
@@ -224,12 +224,7 @@ pub fn wire<S, C, F>(
let ctl = ctl.clone();
window.on_merge_confirm(move || {
let Some(w) = weak.upgrade() else { return };
let projection = match ctl.projection.get() {
1 => Some(dr_pano::Projection::Perspective),
2 => Some(dr_pano::Projection::Cylindrical),
3 => Some(dr_pano::Projection::Spherical),
_ => None,
};
let projection = chip_projection(ctl.projection.get());
if let Some(job) = ctl.job.borrow().as_ref() {
let _ = job.decide.send(Decision::Merge { projection });
job.activity.detail("merging");
@@ -240,9 +235,19 @@ pub fn wire<S, C, F>(
});
}
{
let weak = window.as_weak();
let ctl = ctl.clone();
window.on_merge_projection_picked(move |i| {
let Some(w) = weak.upgrade() else { return };
ctl.projection.set(i);
w.set_merge_projection_selected(i);
// Show it: the job redraws the preview on that surface and
// reports again, and the drain puts the new picture up.
if let Some(job) = ctl.job.borrow().as_ref() {
let _ = job.decide.send(Decision::Preview {
projection: chip_projection(i),
});
}
});
}
@@ -348,6 +353,16 @@ fn start<Fetch>(
*ctl.timer.borrow_mut() = Some(timer);
}
/// The projection a chip index names; 0 is "Auto".
fn chip_projection(i: i32) -> Option<dr_pano::Projection> {
match i {
1 => Some(dr_pano::Projection::Perspective),
2 => Some(dr_pano::Projection::Cylindrical),
3 => Some(dr_pano::Projection::Spherical),
_ => None,
}
}
/// Take everything the job has said and reflect it on the page.
fn drain(window: &AppWindow, ctl: &Rc<MergeController>, on_done: &Rc<impl Fn(&AppWindow)>) {
let events = {