Put the repair tool on the photograph
A third chip beside Crop and Local, and the mode strip's own comment predicted the shape: a mode that arms a gesture on the canvas and scopes the column. Click a mark to cover it, drag the disc to move the repair, drag the source circle to say where the patch comes from, Delete to remove it. The source starts two and a half radii towards the middle of the frame, which is FR-DEV-8's automatic placement in its cheap form — dust sits on skies and skies are smooth, so it is usually right and always one drag from fixed. Two things are drawn deliberately. The circles are the size the repairs actually are, because whether a disc covers a speck is the whole judgement being made and a fixed-size dot would say nothing about it; the reach around them is padded to a touch target so a spot on a dust mark can still be picked up on a phone. And only the selected repair shows its source: a dusty sky carries a dozen, and two dozen circles with nothing saying which belongs to which is less information rather than more. The panel edits what is stored while the canvas draws what is mapped, and the two are pushed separately for that reason — a slider deriving its value from the drawn radius would move differently at different zoom levels. It is also the one panel built from SliderRow rather than a live track: a repair has no OpId to coalesce a drag under, so a row that fires once per gesture is what keeps undo one step per decision. Verified as far as this environment allows: the strip renders and the column re-scopes, photographed under XWayland. Synthetic clicks do not reach this application, so the gestures are as-written rather than as-felt, and docs/spot-removal.md says so. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -89,6 +89,14 @@ pub const MAX_RADIUS: f32 = 0.5;
|
||||
/// a hard edge shows as a disc even when the colour underneath it is right.
|
||||
pub const DEFAULT_FEATHER: f32 = 0.35;
|
||||
|
||||
/// How far a new repair's source starts from what it repairs, in radii.
|
||||
///
|
||||
/// Clear of the disc it is replacing — a source overlapping its own
|
||||
/// destination would copy the mark it is removing — and close enough that on a
|
||||
/// smoothly varying background it is the same background. Two and a half puts
|
||||
/// a full radius of untouched photograph between the two edges.
|
||||
const SOURCE_ARM: f32 = 2.5;
|
||||
|
||||
/// The grid every stored length and coordinate is rounded to, as a divisor.
|
||||
///
|
||||
/// The same value and the same reasoning as [`crate::mask::Stroke`]'s grid:
|
||||
@@ -196,6 +204,44 @@ impl Spot {
|
||||
}
|
||||
}
|
||||
|
||||
/// TRACES: FR-DEV-8
|
||||
/// Where a new repair reads from, before anybody has looked at it.
|
||||
///
|
||||
/// **FR-DEV-8 asks for automatic source placement, and this is the cheap
|
||||
/// half of it.** The good half searches the photograph for a patch whose
|
||||
/// surroundings match — a compute dispatch scoring candidate offsets, and
|
||||
/// one small readback when the spot is created (`docs/spot-removal.md`
|
||||
/// §8). This is what stands in for it, and it is worth having on its own
|
||||
/// terms rather than as a placeholder: dust sits on skies, skies are
|
||||
/// smooth, and a patch two and a half radii away is nearly always the same
|
||||
/// sky.
|
||||
///
|
||||
/// Towards the centre of the frame, because that is the direction with the
|
||||
/// most photograph in it: a mark near an edge sourced outwards reads from
|
||||
/// the border, or from outside it, where `spot_tap` clamps and the repair
|
||||
/// smears. A mark *at* the centre has no such direction and is sent right,
|
||||
/// which is as good as any other bearing and is at least predictable.
|
||||
///
|
||||
/// The result is what gets stored, and it is never recomputed: a repair
|
||||
/// whose source moved on its own when the file was reopened would be an
|
||||
/// edit changing itself, and non-destructive editing means the sidecar
|
||||
/// decides what the picture is.
|
||||
pub fn default_offset(centre: (f32, f32), radius: f32, aspect: f32) -> (f32, f32) {
|
||||
let aspect = if aspect > 0.0 { aspect } else { 1.0 };
|
||||
// In frame units, where a direction is a direction: normalised
|
||||
// coordinates would bend the bearing by the aspect ratio and send a
|
||||
// source off at an angle nobody chose.
|
||||
let to_centre = ((0.5 - centre.0) * aspect, 0.5 - centre.1);
|
||||
let length = to_centre.0.hypot(to_centre.1);
|
||||
let direction = if length > 1e-4 {
|
||||
(to_centre.0 / length, to_centre.1 / length)
|
||||
} else {
|
||||
(1.0, 0.0)
|
||||
};
|
||||
let distance = radius * SOURCE_ARM;
|
||||
(direction.0 * distance, direction.1 * distance)
|
||||
}
|
||||
|
||||
/// TRACES: FR-NC-9
|
||||
/// The id a spot at `centre` is given: a short base-36 hash of the position
|
||||
/// it was placed at.
|
||||
|
||||
Reference in New Issue
Block a user