Split develop.rs into develop/ by area of behaviour
develop.rs had grown to 9,327 lines covering everything the develop session does: opening a photograph, the parameter-row and curve-widget panel model, mask viewing and editing, mask creation and the rasteriser that turns a mask stack into GPU arrays, spot repairs, scene segmentation, framing and zoom, white-balance sampling, rendering and film choice, and the undo/snapshot history. docs/dev/code-health.md CH-1 names dr-ui's lack of a view layer as the reason every feature kept landing in a handful of files; this is the first of the two pure splits it recommends as easy, no-behaviour-change wins independent of that larger rework. The boundaries follow the file's own sections (several were already marked off with comment headers) and the seams a full read turned up underneath them -- mask storage/rasterisation turned out to be a distinct concern from mask viewing and editing, and rows/tabs/curves from each other, so those split further than the headers alone suggested. Each module stays under about 1,500 lines. Struct fields and the handful of helper methods now called from a sibling module became `pub(super)`, which is strictly narrower than the whole-crate reachability a single file gave them; nothing gained visibility outside `develop`. Tests moved with the code they test, including the few cases where a helper one file's tests needed was itself only defined in another's -- those became shared fixtures in `mod.rs` alongside the `headless`/`read_back`/`grey_session` helpers that already worked that way. `mod.rs` re-exports every item `develop::` callers outside this module used before, so lib.rs, masks_ui.rs and the rest needed no changes.
This commit is contained in:
@@ -0,0 +1,241 @@
|
||||
//! Spot removal (FR-DEV-8): placing, dragging and clearing heal/clone spots.
|
||||
use dr_pipeline::Edit;
|
||||
|
||||
use crate::labels;
|
||||
|
||||
use super::session::DevelopSession;
|
||||
|
||||
impl DevelopSession {
|
||||
// --- repairs (FR-DEV-8) ------------------------------------------------
|
||||
|
||||
/// TRACES: FR-DEV-8
|
||||
/// Cover what is at `(x, y)`, in fractions of the shown image.
|
||||
///
|
||||
/// The click arrives in *output* coordinates — where the photograph
|
||||
/// currently sits on screen — and a repair is stored against the
|
||||
/// photograph, so it goes through `Framing::source_at`: the same map the
|
||||
/// shader applies, run backwards. Anything less would put the repair where
|
||||
/// the pointer was rather than where the mark is, and the two agree only at
|
||||
/// fit-to-window with no crop.
|
||||
///
|
||||
/// Returns the new repair's id, or `None` when the set is full. Selecting
|
||||
/// it is deliberate: the control that changes its size is in the column,
|
||||
/// and a photographer who has just placed a spot too small should find that
|
||||
/// control already pointed at it.
|
||||
pub fn place_spot(&mut self, x: f32, y: f32) -> Option<String> {
|
||||
let (sw, sh) = self.demosaiced.size();
|
||||
let centre = self.graph.framing().source_at((x, y), sw, sh);
|
||||
// Outside the photograph entirely — the letterbox margin, or a drag
|
||||
// that ended off the edge. Placing a repair there would put a disc
|
||||
// somewhere the user cannot see and cannot pick up again.
|
||||
if !(0.0..=1.0).contains(¢re.0) || !(0.0..=1.0).contains(¢re.1) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let aspect = sw.max(1) as f32 / sh.max(1) as f32;
|
||||
let radius = dr_pipeline::spot::DEFAULT_RADIUS;
|
||||
let offset = dr_pipeline::Spot::default_offset(centre, radius, aspect);
|
||||
|
||||
let id = self
|
||||
.graph
|
||||
.spots_mut()
|
||||
.place(dr_pipeline::Spot::new(centre, offset, radius))?;
|
||||
self.selected_spot = Some(id.clone());
|
||||
self.history
|
||||
.record(&self.graph, Edit::Action(labels::step::SPOT_PLACED));
|
||||
Some(id)
|
||||
}
|
||||
|
||||
/// TRACES: FR-DEV-8 | FR-UI-3
|
||||
/// Every repair as a circle on the shown image, plus the source circle of
|
||||
/// the selected one.
|
||||
///
|
||||
/// # Why only the selected repair shows its source
|
||||
///
|
||||
/// A dusty sky carries a dozen repairs. Two dozen circles with nothing
|
||||
/// saying which source belongs to which disc is not more information, it is
|
||||
/// less — and there is no room on a phone for a connector between each
|
||||
/// pair. The selection is what disambiguates them, which is also why a
|
||||
/// press on a repair selects it before the drag begins.
|
||||
///
|
||||
/// Recomputed per redraw rather than cached, for the reason
|
||||
/// [`Self::gradient_handles`] gives: the answer changes with the *view*,
|
||||
/// and a pan moves every circle while touching no edit.
|
||||
pub fn spot_handles(&self) -> Vec<crate::SpotHandle> {
|
||||
let (sw, sh) = self.demosaiced.size();
|
||||
let framing = self.graph.framing();
|
||||
let aspect = sw.max(1) as f32 / sh.max(1) as f32;
|
||||
// The shown image's own shape, which is not the source's once the frame
|
||||
// has been cropped or turned. A radius is reported against its height,
|
||||
// so this is what converts the x half of the mapped offset.
|
||||
let (ow, oh) = self.graph.output_size(sw, sh);
|
||||
let shown_aspect = ow.max(1) as f32 / oh.max(1) as f32;
|
||||
|
||||
let mut handles = Vec::new();
|
||||
for spot in self.graph.spots().spots() {
|
||||
let selected = self.selected_spot.as_deref() == Some(spot.id.as_str());
|
||||
let centre = framing.output_at(spot.centre, sw, sh);
|
||||
|
||||
// The radius, mapped rather than scaled: a point one radius above
|
||||
// the centre goes through the same map, and the distance between
|
||||
// the two answers is the radius as drawn. The x half is multiplied
|
||||
// by the shown aspect because the two axes are normalised by
|
||||
// different lengths, and a circle measured in mixed units is an
|
||||
// ellipse.
|
||||
let rim = framing.output_at((spot.centre.0, spot.centre.1 + spot.radius), sw, sh);
|
||||
let radius = ((rim.0 - centre.0) * shown_aspect).hypot(rim.1 - centre.1);
|
||||
|
||||
handles.push(crate::SpotHandle {
|
||||
id: spot.id.clone().into(),
|
||||
role: crate::SpotRole::Destination,
|
||||
x: centre.0,
|
||||
y: centre.1,
|
||||
radius,
|
||||
selected,
|
||||
enabled: spot.enabled,
|
||||
});
|
||||
|
||||
if selected {
|
||||
let source = framing.output_at(spot.source(aspect), sw, sh);
|
||||
handles.push(crate::SpotHandle {
|
||||
id: spot.id.clone().into(),
|
||||
role: crate::SpotRole::Source,
|
||||
x: source.0,
|
||||
y: source.1,
|
||||
radius,
|
||||
selected: true,
|
||||
enabled: spot.enabled,
|
||||
});
|
||||
}
|
||||
}
|
||||
handles
|
||||
}
|
||||
|
||||
/// TRACES: FR-DEV-8
|
||||
/// Drag one circle of one repair, from `press` to `now`, both in fractions
|
||||
/// of the shown image.
|
||||
///
|
||||
/// Dragging the disc moves the whole repair and carries its source along —
|
||||
/// what a photographer means by nudging a spot. Dragging the source moves
|
||||
/// the source alone, which is the override FR-DEV-8 asks for over the
|
||||
/// automatic placement.
|
||||
///
|
||||
/// `origin` is the repair as it stood when the gesture began; the caller
|
||||
/// holds it for the duration and hands it back, so a drag is applied to
|
||||
/// that rather than accumulated frame by frame — the rule
|
||||
/// [`Self::drag_gradient_handle`] states, for the same reasons.
|
||||
pub fn drag_spot(
|
||||
&mut self,
|
||||
id: &str,
|
||||
role: crate::SpotRole,
|
||||
origin: Option<&dr_pipeline::Spot>,
|
||||
press: (f32, f32),
|
||||
now: (f32, f32),
|
||||
) -> Option<dr_pipeline::Spot> {
|
||||
let (sw, sh) = self.demosaiced.size();
|
||||
let framing = *self.graph.framing();
|
||||
let aspect = sw.max(1) as f32 / sh.max(1) as f32;
|
||||
|
||||
let start = match origin {
|
||||
Some(spot) => spot.clone(),
|
||||
None => self.graph.spots().get(id)?.clone(),
|
||||
};
|
||||
|
||||
// The displacement in source coordinates. Affine, so a movement is a
|
||||
// movement: the map may be run on the two endpoints and subtracted,
|
||||
// which is what makes a drag on a rotated photograph move the repair in
|
||||
// the direction the finger went.
|
||||
let from = framing.source_at(press, sw, sh);
|
||||
let to = framing.source_at(now, sw, sh);
|
||||
let moved = (to.0 - from.0, to.1 - from.1);
|
||||
|
||||
let spot = self.graph.spots_mut().get_mut(id)?;
|
||||
match role {
|
||||
crate::SpotRole::Destination => {
|
||||
spot.set_centre((start.centre.0 + moved.0, start.centre.1 + moved.1));
|
||||
}
|
||||
// In frame units, because that is what an offset is stored in — and
|
||||
// the x half of a normalised displacement is short by the aspect.
|
||||
crate::SpotRole::Source => {
|
||||
spot.set_offset((start.offset.0 + moved.0 * aspect, start.offset.1 + moved.1));
|
||||
}
|
||||
}
|
||||
// Nothing recorded here: a drag delivers a pointer event a frame, and
|
||||
// one history step apiece would make undo walk the gesture back pixel
|
||||
// by pixel. Recorded once, on release.
|
||||
Some(start)
|
||||
}
|
||||
|
||||
/// A repair's drag finished: one history step for the whole gesture.
|
||||
pub fn commit_spot_drag(&mut self) {
|
||||
self.history
|
||||
.record(&self.graph, Edit::Action(labels::step::SPOT_MOVED));
|
||||
}
|
||||
|
||||
/// Which repair the column is describing.
|
||||
pub fn selected_spot(&self) -> Option<&dr_pipeline::Spot> {
|
||||
let id = self.selected_spot.as_deref()?;
|
||||
self.graph.spots().get(id)
|
||||
}
|
||||
|
||||
pub fn selected_spot_id(&self) -> Option<&str> {
|
||||
self.selected_spot.as_deref()
|
||||
}
|
||||
|
||||
/// Choose a repair, or `None` to describe none.
|
||||
///
|
||||
/// An id the graph no longer holds selects nothing rather than being kept:
|
||||
/// the circle that offered it is stale by the time the press lands, and a
|
||||
/// selection pointing at a deleted repair would leave the column describing
|
||||
/// something that is not on the photograph.
|
||||
pub fn select_spot(&mut self, id: Option<&str>) {
|
||||
self.selected_spot = id
|
||||
.filter(|id| self.graph.spots().get(id).is_some())
|
||||
.map(str::to_string);
|
||||
}
|
||||
|
||||
/// TRACES: FR-DEV-8
|
||||
/// Take a repair off the photograph, returning whether one went.
|
||||
pub fn remove_spot(&mut self, id: &str) -> bool {
|
||||
if self.graph.spots_mut().remove(id).is_none() {
|
||||
return false;
|
||||
}
|
||||
if self.selected_spot.as_deref() == Some(id) {
|
||||
self.selected_spot = None;
|
||||
}
|
||||
self.history
|
||||
.record(&self.graph, Edit::Action(labels::step::SPOT_REMOVED));
|
||||
true
|
||||
}
|
||||
|
||||
/// TRACES: FR-DEV-8
|
||||
/// Change one of the selected repair's settings.
|
||||
///
|
||||
/// Recorded as a named [`Edit::Action`] rather than under a parameter key:
|
||||
/// a repair is not an operation and has no `OpId` to name or coalesce by,
|
||||
/// so a slider drag over it records a step per movement unless the caller
|
||||
/// debounces. `SliderRow` fires once per completed gesture,
|
||||
/// which is what makes that acceptable here and is why this is the one
|
||||
/// panel in the application built from that row rather than from a live
|
||||
/// track.
|
||||
pub fn set_selected_spot<F>(&mut self, change: F) -> bool
|
||||
where
|
||||
F: FnOnce(&mut dr_pipeline::Spot),
|
||||
{
|
||||
let Some(id) = self.selected_spot.clone() else {
|
||||
return false;
|
||||
};
|
||||
let Some(spot) = self.graph.spots_mut().get_mut(&id) else {
|
||||
return false;
|
||||
};
|
||||
change(spot);
|
||||
self.history
|
||||
.record(&self.graph, Edit::Action(labels::step::SPOT));
|
||||
true
|
||||
}
|
||||
|
||||
/// How many repairs this photograph carries.
|
||||
pub fn spot_count(&self) -> usize {
|
||||
self.graph.spots().len()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user