//! TRACES: FR-DEV-6 //! The presets a first run starts with. //! //! # Why any at all //! //! A preset sheet that opens on "No presets yet" teaches the photographer that //! the feature is homework. These exist so the first thing the sheet does is //! demonstrate what a preset *is* — and so that applying one to a selection, //! which is the action worth discovering, is available before anybody has //! saved anything. //! //! # Why these are ours and not Adobe's //! //! Lightroom ships a large bundled set, and importing one of *those* files is //! what [`crate::preset_import`] is for — a photographer's own library, //! carried across. Redistributing Adobe's inside this application would be //! shipping their creative work under a licence that does not permit it, which //! is a reason on its own; and their numbers are calibrated against their tone //! curve rather than ours, so the look would not survive the trip even if the //! licence allowed it. //! //! So these are written against this pipeline, in its units, and they are //! deliberately mild. A starter preset is a starting point — a photographer //! who wanted the full effect can push the sliders, where one who is handed a //! caricature learns to distrust the list. //! //! # Why they live in the core rather than in the interface //! //! Because they name operations, and nothing in `ui/` may //! (`ui_names_no_operation.rs`, ARCH §4.3a). That test is right to object: a //! preset called "Punch" *is* a statement about contrast, clarity and //! vibrance, which makes it a statement in the pipeline's vocabulary rather //! than a fact about any interface. The frontend asks for the set and stores //! it; it never learns what is in it. //! //! # Why they are seeded rather than merged //! //! Written once, on the first run that finds no library at all, and never //! again. Re-adding them on every start would resurrect one the photographer //! deleted on purpose, and updating them in place would silently rewrite an //! edit they had adjusted and kept under the same name. After the first run //! these are ordinary presets: renameable, editable, deletable, and gone for //! good when deleted. use std::collections::BTreeMap; use crate::{Preset, PresetLibrary}; /// One starter preset: a name and the parameters that differ from default. struct Starter { name: &'static str, params: &'static [(&'static str, &'static str, f32)], } /// The set. Small on purpose — six a photographer might actually reach for /// beats forty they have to scroll past. const STARTERS: &[Starter] = &[ Starter { name: "Punch", params: &[ ("contrast", "contrast", 18.0), ("clarity", "amount", 12.0), ("vibrance", "vibrance", 18.0), ("blacks_whites", "blacks", -8.0), ], }, Starter { name: "Soft portrait", params: &[ ("contrast", "contrast", -8.0), ("highlights_shadows", "highlights", -20.0), ("highlights_shadows", "shadows", 15.0), ("clarity", "amount", -10.0), ("vibrance", "vibrance", 10.0), ("saturation", "saturation", -5.0), ], }, Starter { name: "Recover the sky", params: &[ // The most common single fix in landscape work: a bright sky and a // dark foreground, both pulled back toward the middle. ("highlights_shadows", "highlights", -55.0), ("highlights_shadows", "shadows", 35.0), ("blacks_whites", "whites", -10.0), ], }, Starter { name: "Lift the shadows", params: &[ ("highlights_shadows", "shadows", 40.0), ("blacks_whites", "blacks", 12.0), ("contrast", "contrast", -5.0), ], }, Starter { name: "Crisp detail", params: &[ ("texture", "amount", 20.0), ("clarity", "amount", 10.0), ("capture_sharpen", "amount", 35.0), ], }, Starter { name: "Muted", params: &[ ("saturation", "saturation", -30.0), ("vibrance", "vibrance", 10.0), ("contrast", "contrast", -10.0), ("highlights_shadows", "shadows", 12.0), ], }, ]; /// The starter library, for a device that has never had one. pub fn library() -> PresetLibrary { let mut library = PresetLibrary::default(); for starter in STARTERS { let params: BTreeMap<(String, String), f32> = starter .params .iter() .map(|(op, param, value)| ((op.to_string(), param.to_string()), *value)) .collect(); // The name is a literal in this file, so a refusal would be a bug here // rather than bad input — but it still must not take the whole set // down, since the alternative to five presets is not six, it is none. if let Err(e) = library.insert(starter.name, Preset::from_params(params)) { log::warn!( "starter preset {:?} is unusable ({e:?}); skipping", starter.name ); } } library } #[cfg(test)] mod tests { use super::*; use crate::{EditGraph, Scope}; #[test] fn every_starter_names_parameters_this_build_actually_has() { // The same guard the importer's table has, for the same reason: a // renamed parameter must break the build rather than ship a preset // that quietly does nothing. let graph = EditGraph::default_chain(); let capabilities = graph.capabilities(); for starter in STARTERS { for (op, param, _) in starter.params { let capability = capabilities .iter() .find(|c| c.id.0 == *op) .unwrap_or_else(|| panic!("{:?}: no operation {op:?}", starter.name)); assert!( capability.params.iter().any(|p| p.id.0 == *param), "{:?}: operation {op:?} has no parameter {param:?}", starter.name ); } } } #[test] fn every_starter_actually_changes_something() { // A preset that applies to nothing is worse than one fewer preset: it // teaches the photographer that the list does not work. for (name, preset) in library().iter() { assert!(!preset.is_empty(), "{name} carries nothing"); let mut graph = EditGraph::default_chain(); preset.apply(&mut graph, Scope::adjustments()); assert_ne!( Preset::capture(&graph), Preset::default(), "{name} left the graph at its defaults" ); } } #[test] fn no_starter_carries_a_crop() { // These are looks, not compositions. One that re-framed every image it // was applied to would be the exact accident `Scope`'s default exists // to prevent. for (name, preset) in library().iter() { assert!(!preset.touches_framing(), "{name} carries framing"); } } #[test] fn the_names_are_distinct() { assert_eq!(library().len(), STARTERS.len()); } #[test] fn the_values_stay_inside_what_the_controls_accept() { // Clamping happens on apply, so an out-of-range literal here would be // silently trimmed and the preset would not be the one written. let graph = EditGraph::default_chain(); for starter in STARTERS { for (op, param, value) in starter.params { let mut applied = EditGraph::default_chain(); let capability = graph .capabilities() .into_iter() .find(|c| c.id.0 == *op) .unwrap(); let descriptor = capability.params.iter().find(|p| p.id.0 == *param).unwrap(); applied.set_param(capability.id, descriptor.id, *value); assert_eq!( applied.param(capability.id, descriptor.id), Some(*value), "{}: {op}.{param} = {value} was clamped", starter.name ); } } } }