mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-28 12:08:10 -06:00
@@ -18,6 +18,11 @@ use crate::geometry::Frame;
|
||||
|
||||
#[inline]
|
||||
fn smooth(e0: f32, e1: f32, x: f32) -> f32 {
|
||||
// Zero feather is a hard edge, including when the pixel lies exactly on the radius.
|
||||
// Dividing 0 by 0 here would give that pixel a NaN alpha, even at zero opacity.
|
||||
if e0 >= e1 {
|
||||
return if x < e1 { 0.0 } else { 1.0 };
|
||||
}
|
||||
let t = ((x - e0) / (e1 - e0)).clamp(0.0, 1.0);
|
||||
t * t * (3.0 - 2.0 * t)
|
||||
}
|
||||
@@ -381,6 +386,51 @@ mod tests {
|
||||
use super::*;
|
||||
use lightcraft_develop::DevelopSettings;
|
||||
|
||||
#[test]
|
||||
fn hard_edge_spots_at_zero_opacity_leave_every_pixel_unchanged() {
|
||||
for mode in [SpotMode::Clone, SpotMode::Heal, SpotMode::Remove, SpotMode::Fill] {
|
||||
for (cx, cy) in [(32.5, 24.5), (0.5, 0.5)] {
|
||||
let original = Rgb32f::from_fn(64, 48, |x, y| [0.2 + x as f32 / 128.0, 0.3 + y as f32 / 128.0, 0.4]);
|
||||
let mut img = original.clone();
|
||||
let frame = Frame::new(64, 48, &DevelopSettings::default(), true);
|
||||
let spot = Spot {
|
||||
mode,
|
||||
points: vec![Point::new(cx / 64.0, cy / 48.0)],
|
||||
size: 5.0 / 64.0,
|
||||
feather: 0.0,
|
||||
opacity: 0.0,
|
||||
source_offset: Some(Point::new(8.0 / 64.0, 0.0)),
|
||||
};
|
||||
apply(&mut img, &[spot], &frame, frame.px_per_long(64));
|
||||
for (i, (actual, expected)) in img.data.iter().zip(&original.data).enumerate() {
|
||||
assert_eq!(actual, expected, "zero-opacity {mode:?} at ({cx}, {cy}), pixel {i}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hard_edge_clone_copies_inside_and_preserves_pixels_on_the_radius() {
|
||||
for (w, h, cx, cy, radius) in [(64, 48, 32.5, 24.5, 5.0), (128, 96, 64.5, 48.5, 10.0)] {
|
||||
let original = Rgb32f::from_fn(w, h, |x, y| [0.2 + x as f32 / w as f32, y as f32 / h as f32, 0.4]);
|
||||
let mut img = original.clone();
|
||||
let frame = Frame::new(w, h, &DevelopSettings::default(), true);
|
||||
let spot = Spot {
|
||||
mode: SpotMode::Clone,
|
||||
points: vec![Point::new(cx / w as f64, cy / h as f64)],
|
||||
size: radius / w as f64,
|
||||
feather: 0.0,
|
||||
opacity: 100.0,
|
||||
source_offset: Some(Point::new(0.125, 0.0)),
|
||||
};
|
||||
apply(&mut img, &[spot], &frame, frame.px_per_long(w));
|
||||
let (x, y) = (cx as usize, cy as usize);
|
||||
assert_eq!(img.get(x, y), original.get(x + w / 8, y), "inside copies the source at {w}px");
|
||||
assert_eq!(img.get(x + radius as usize, y), original.get(x + radius as usize, y), "radius stays unchanged at {w}px");
|
||||
assert!(img.data.iter().flatten().all(|v| v.is_finite()));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remove_erases_a_dot() {
|
||||
// flat grey with a gentle gradient and a black dot in the middle
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** minor (LR-EDIT-OPTICS-PROFILE: Nikon Z 6II / Z f in-file distortion correction; LR-MERGE-HDR: merges of raws without colour matrices keep their colour (#965); rapid slider entry; LR-PROF-CAMERACOLOR: tone curve from black-and-white camera JPEGs; LR-VIEW-ZOOM, LR-VIEW-COMPARE: Compare and Reference zoom windows; LR-IMP-FORMATS, LR-IMP-CAMERA-COVERAGE: EOS R5 Mark II CR3 decodes; renamed from `parity.md`; Top gaps moved to gaps.md, shortcut notes to ui-parity.md; filesystem history retry/Undo/Redo evidence; renamed from `parity.md`; Top gaps moved to gaps.md, shortcut notes to ui-parity.md) · **Target:** Adobe Lightroom Classic 15.5 / Lightroom 9.6
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (rapid slider entry; LR-PROF-CAMERACOLOR: tone curve from black-and-white camera JPEGs; LR-VIEW-ZOOM, LR-VIEW-COMPARE: Compare and Reference zoom windows; LR-IMP-FORMATS, LR-IMP-CAMERA-COVERAGE: EOS R5 Mark II CR3 decodes; renamed from `parity.md`; Top gaps moved to gaps.md, shortcut notes to ui-parity.md; filesystem history retry/Undo/Redo evidence; renamed from `parity.md`; Top gaps moved to gaps.md, shortcut notes to ui-parity.md; Model folder/error/retry) · **Target:** Adobe Lightroom Classic 15.5 / Lightroom 9.6
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** minor (LRC-LIB-CATALOG-IMPORT: catalogs whose tables are all in the WAL open; LR-MERGE-HDR: merges of raws without colour matrices keep their colour (#965); rapid slider entry; LR-PROF-CAMERACOLOR: tone curve from black-and-white camera JPEGs; LR-VIEW-ZOOM, LR-VIEW-COMPARE: Compare and Reference zoom windows; LR-IMP-FORMATS, LR-IMP-CAMERA-COVERAGE: EOS R5 Mark II CR3 decodes; renamed from `parity.md`; Top gaps moved to gaps.md, shortcut notes to ui-parity.md; filesystem history retry/Undo/Redo evidence; renamed from `parity.md`; Top gaps moved to gaps.md, shortcut notes to ui-parity.md) · **Target:** Adobe Lightroom Classic 15.5 / Lightroom 9.6
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-14 · **Change:** minor (LR-MERGE-HDR: merges of raws without colour matrices keep their colour (#965); rapid slider entry; LR-PROF-CAMERACOLOR: tone curve from black-and-white camera JPEGs; LR-VIEW-ZOOM, LR-VIEW-COMPARE: Compare and Reference zoom windows; LR-IMP-FORMATS, LR-IMP-CAMERA-COVERAGE: EOS R5 Mark II CR3 decodes; renamed from `parity.md`; Top gaps moved to gaps.md, shortcut notes to ui-parity.md; filesystem history retry/Undo/Redo evidence; renamed from `parity.md`; Top gaps moved to gaps.md, shortcut notes to ui-parity.md) · **Target:** Adobe Lightroom Classic 15.5 / Lightroom 9.6
|
||||
|
||||
The measured, row-by-row checklist: one row per Lightroom feature, menu item and shortcut, what we
|
||||
have and what is missing. It answers *does the feature exist?* (feature breadth). Whether it is good
|
||||
@@ -267,8 +268,8 @@ differences and missing bindings are in [ui-parity.md](ui-parity.md#shortcuts).
|
||||
| Id | Feature | Tier | Status | Evidence | Missing / notes |
|
||||
|---|---|---|---|---|---|
|
||||
| LR-REM-CONTENTAWARE | Content-aware remove | P1 | 🟡 | `cmd:spot.add` (`mode: remove` / `fill`), `crates/pipeline/src/spots.rs`, `crates/inpaint/src/lib.rs` | Remove heals with an automatic source; Fill synthesises the area from its surroundings (PatchMatch completion over a membrane-blended start, `crates/inpaint`, ported from PhotoCraft; bounded by the spot size limit); no generative fill (M8.4) |
|
||||
| LR-REM-HEAL | Heal | P0 | ✅ | `cmd:spot.add` (`mode: heal`) | |
|
||||
| LR-REM-CLONE | Clone | P0 | ✅ | `cmd:spot.add` (`mode: clone`) | |
|
||||
| LR-REM-HEAL | Heal | P0 | ✅ | `cmd:spot.add` (`mode: heal`), `crates/pipeline/src/spots.rs` | Zero-feather edges use a hard step; zero-opacity spots preserve all pixels, including exact-radius pixels and image corners |
|
||||
| LR-REM-CLONE | Clone | P0 | ✅ | `cmd:spot.add` (`mode: clone`), `crates/pipeline/src/spots.rs` | Hard-edge source copying and unchanged radius pixels regression-tested at two output sizes |
|
||||
| LR-REM-GEN | Generative remove | OOS | 🚫 | | |
|
||||
| LR-REM-DETECT | Object detection for remove | P2 | ⬜ | | |
|
||||
| LR-REM-BRUSH-PARAMS | Brush size / feather / opacity | P0 | ✅ | `cmd:spot.add` (`size`, `feather`, `opacity`), `cmd:brush.smaller`, `cmd:brush.larger`, `cmd:brush.featherLess`, `cmd:brush.featherMore`, `crates/ui-egui/src/panels/right.rs` | Size / Feather / Opacity sliders (for new spots and the selected one); `[` `]` size, ⇧`[` ⇧`]` feather (also the Masking brush) |
|
||||
@@ -799,6 +800,7 @@ Latin stays upright. Advanced Japanese composition remains open. Regression cove
|
||||
| 2026-10-13 | patch | LR-VIEW-COMPARE, LRC-LIB-COMPARE: ⌘/Ctrl- and Shift-clicks on the filmstrip in Compare change the selection as in the grid; the two photos shown stay selected and the active photo is unchanged (#187); statuses unchanged |
|
||||
| 2026-10-13 | minor | KEYC-SELECTONLYACTIVE: Edit → Select Only Active Photo (⌘⇧D) lands as a new row; status 🟡 (headless-tested, native macOS menu path unverified) |
|
||||
| 2026-10-13 | patch | LRC-LIB-CATALOG-IMPORT: a catalog whose tables are all still in its `-wal` file (main file schema format 0) opens instead of failing as an unsupported schema format; status unchanged |
|
||||
| 2026-10-14 | patch | LR-REM-HEAL, LR-REM-CLONE: zero-feather edges preserve exact-radius pixels; zero-opacity regression covers all spot modes and image corners. Statuses and feature counts unchanged. |
|
||||
| 2026-10-11 | minor | LR-MERGE-HDR: HDR and panorama merges of raws without colour matrices (CR2, NEF, ARW…) write the colour they open with as DNG tags (issue #965); status unchanged |
|
||||
| 2026-10-12 | minor | LR-META-XMP: Read Metadata from File no longer auto-writes the sidecar it has just read, which added `lc:settings` and hid another app's later `crs:` edits (#934); status unchanged (✅) |
|
||||
| 2026-10-12 | patch | LR-IMP-CAMERA-COVERAGE: CR2 white level from the active area, not the saturated masked rows above it (issue #1272); status unchanged (🟡) |
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# What's new in LightCraft
|
||||
|
||||
## 2026-10-14
|
||||
|
||||
### Hard-edged Heal and Clone spots
|
||||
- A spot with Feather at 0 no longer draws black pixels exactly on its radius. This also fixes spots at 0% Opacity
|
||||
changing pixels along their edge. The hard edge now leaves pixels on the radius unchanged.
|
||||
|
||||
## 2026-10-13
|
||||
|
||||
### HDR merges keep the lens correction
|
||||
|
||||
Reference in New Issue
Block a user