Let a photographer choose the film, and remember which one
The stock model rendered correctly and nothing could ask for it. This is the picker, and the sidecar key that makes the choice outlive the session. How the choice persists was the open question, and the answer was already written down twice in sidecar.rs: `rating` is a top-level key "because a rating is not an edit", and `masks` are one "because a layer is not a scalar". A stock is that kind of thing -- a choice of material, not a number a slider moves -- so it is a top-level key too. It stores the **id**, not an index. Stocks are files that users add, so an index would mean installing a profile silently changed which film every existing photograph had been developed on. A name this build has no profile for still round-trips untouched, because the alternative is that syncing to an older phone quietly un-develops the picture. Only the names travel. Turning one back into tables needs the profile database, which dr-pipeline deliberately does not link, so `Version::apply` clears the film and the session re-bakes -- after the parameters, because the bake reads the film's own exposure sliders and the print balance is solved against them. That is also why moving those sliders rebuilds the lookup where no other control in the panel does: an enlarger's filtration depends on how the negative was exposed. The panel keeps its rule. It still names no operation and still generates every control from a declared parameter kind; the stock gets a bespoke control beside those, exactly as the mask stack does, and for the same reason. The film's exposure and print exposure arrive as ordinary generated sliders. Two defaults worth stating. Picking a colour negative prints it, because an unprinted one is an orange strip and offering that as the first thing somebody sees after choosing Portra reads as a bug rather than as a choice -- the toggle is there for anyone who wants the scan. And a paste carries no film: a preset is a parameter map, and a stock is not a parameter, so pasting one would paste a choice the clipboard never took. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -509,6 +509,46 @@ fn apply_when_ready(
|
||||
);
|
||||
}
|
||||
|
||||
/// TRACES: FR-DEV-3f
|
||||
/// Push the film choice out to the panel.
|
||||
///
|
||||
/// Separate from [`sync_rows`] because the stock is not a row: it is not a
|
||||
/// parameter, so it is not in the model the panel generates its controls from.
|
||||
/// Called from the two handlers that change it, and once when an image opens.
|
||||
pub(crate) fn sync_film(window: &AppWindow, session: &Rc<RefCell<Option<DevelopSession>>>) {
|
||||
let choices = DevelopSession::film_choices();
|
||||
|
||||
let chosen = session.borrow().as_ref().and_then(|s| {
|
||||
s.film()
|
||||
.map(|(stock, print)| (stock.to_string(), print))
|
||||
});
|
||||
|
||||
// A stock this build has no profile for leaves the picker on "None" rather
|
||||
// than inventing an entry for it. The sidecar still carries the name — it
|
||||
// is only the *control* that cannot show what it does not have.
|
||||
let selected = chosen
|
||||
.as_ref()
|
||||
.and_then(|(stock, _)| choices.iter().position(|(id, _)| *id == Some(stock.as_str())))
|
||||
.unwrap_or(0);
|
||||
|
||||
let can_print = choices
|
||||
.get(selected)
|
||||
.and_then(|(id, _)| *id)
|
||||
.and_then(dr_film::find)
|
||||
.and_then(dr_film::default_print)
|
||||
.is_some();
|
||||
|
||||
window.set_film_stocks(slint::ModelRc::new(slint::VecModel::from(
|
||||
choices
|
||||
.into_iter()
|
||||
.map(|(_, name)| slint::SharedString::from(name))
|
||||
.collect::<Vec<_>>(),
|
||||
)));
|
||||
window.set_film_selected(selected as i32);
|
||||
window.set_film_can_print(can_print);
|
||||
window.set_film_print(chosen.map(|(_, print)| print).unwrap_or(false));
|
||||
}
|
||||
|
||||
pub(crate) fn sync_rows(
|
||||
window: &AppWindow,
|
||||
rows: &Rc<slint::VecModel<ParamRow>>,
|
||||
@@ -1852,6 +1892,13 @@ pub fn run(paths: Vec<PathBuf>) -> Result<()> {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
if let Some(s) = session.borrow_mut().as_mut() {
|
||||
s.set_param(op, param, value);
|
||||
// TRACES: FR-DEV-3f
|
||||
// The film's own exposures ride *inside* the baked tables
|
||||
// rather than arriving as uniforms, because the print balance
|
||||
// is solved against them — an enlarger's filtration depends on
|
||||
// how the negative was exposed. So moving one has to rebuild
|
||||
// the lookup, which no other slider in the panel does.
|
||||
s.rebake_film_if_affected(op);
|
||||
}
|
||||
sync_rows(&w, &rows, &session);
|
||||
redraw(&w);
|
||||
@@ -1942,6 +1989,57 @@ pub fn run(paths: Vec<PathBuf>) -> Result<()> {
|
||||
});
|
||||
}
|
||||
|
||||
// ---- the film stock (FR-DEV-3f) -------------------------------------
|
||||
//
|
||||
// Unlike the curve channel above, both of these change the photograph:
|
||||
// choosing a stock *is* the edit. So they redraw, and they leave the
|
||||
// session dirty for the sidecar in the ordinary way.
|
||||
{
|
||||
let weak = window.as_weak();
|
||||
let session = session.clone();
|
||||
let redraw = redraw.clone();
|
||||
let rows = rows.clone();
|
||||
window.on_film_picked(move |index| {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
// Index zero is "None" — see `DevelopSession::film_choices`, which
|
||||
// puts it first so that the neutral choice needs no sentinel.
|
||||
let choices = DevelopSession::film_choices();
|
||||
let Some((stock, _)) = choices.get(index.max(0) as usize) else {
|
||||
return;
|
||||
};
|
||||
if let Some(s) = session.borrow_mut().as_mut() {
|
||||
// Printed by default when the stock has a paper: a colour
|
||||
// negative that has not been printed is an orange strip, and
|
||||
// offering that as the first thing a photographer sees when
|
||||
// they pick Portra would read as a bug rather than as a
|
||||
// choice. The toggle is there for anyone who wants the scan.
|
||||
let print = stock
|
||||
.and_then(dr_film::find)
|
||||
.and_then(dr_film::default_print)
|
||||
.is_some();
|
||||
s.choose_film(*stock, print);
|
||||
}
|
||||
sync_film(&w, &session);
|
||||
sync_rows(&w, &rows, &session);
|
||||
redraw(&w);
|
||||
});
|
||||
}
|
||||
{
|
||||
let weak = window.as_weak();
|
||||
let session = session.clone();
|
||||
let redraw = redraw.clone();
|
||||
window.on_film_print_toggled(move |print| {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
if let Some(s) = session.borrow_mut().as_mut() {
|
||||
if let Some((stock, _)) = s.film().map(|(a, b)| (a.to_string(), b)) {
|
||||
s.choose_film(Some(&stock), print);
|
||||
}
|
||||
}
|
||||
sync_film(&w, &session);
|
||||
redraw(&w);
|
||||
});
|
||||
}
|
||||
|
||||
// ---- undo and redo (FR-DEV-5) ---------------------------------------
|
||||
//
|
||||
// Thin, because the history lives in the session and every mutator there
|
||||
|
||||
Reference in New Issue
Block a user