mirror of
https://github.com/storytold/lightcraft.git
synced 2026-11-01 05:57:09 -06:00
Merge PR #2639: LR-MASK-BRUSH: with the mask overlay hidden, the stroke being painted no longer tints the photo (#2549)
crates/ui-egui/src/panels/detail.rs: kept main's split; hunk ported to panels/detail/masks.rs docs/parity-checklist.md: header and LR-MASK-BRUSH row carry both sides
This commit is contained in:
@@ -487,10 +487,21 @@ fn brush_tool(app: &mut LightcraftApp, ui: &mut egui::Ui, resp: &egui::Response,
|
||||
}
|
||||
}
|
||||
// the stroke being painted (the render shows it once it's committed), with the committed
|
||||
// stroke's dab spacing, feather and flow (issue #517)
|
||||
if let Some(Gesture::Brush { points, pressure }) = &app.gesture {
|
||||
// stroke's dab spacing, feather and flow (issue #517), tinted only when the overlay will
|
||||
// show it: with the overlay hidden (O, or a mask slider hid it) or a colour overlay at
|
||||
// opacity 0, the photo stays untinted while painting, so dodging and burning is judged on
|
||||
// the photo (issue #2549)
|
||||
use lightcraft_pipeline::MaskView;
|
||||
let colored = matches!(MaskView::parse(&app.ui.mask_overlay_mode).unwrap_or_default(), MaskView::Color | MaskView::ColorOnBw);
|
||||
// the all-masks overlay ([`view_overlay`]) shows only visible masks, and gives way to the
|
||||
// selected mask's when none has anything in it (a stroke goes into the selected mask, or a new one)
|
||||
let visible = active.and_then(|id| d.masks.iter().find(|m| m.id == id)).is_none_or(|m| m.visible);
|
||||
let all = app.ui.mask_overlay_all && (visible || d.masks.iter().any(|m| m.visible && !m.components.is_empty()));
|
||||
let shown = if all { visible } else { app.ui.mask_overlay } && !(colored && app.ui.mask_overlay_opacity <= 0.0);
|
||||
if shown && let Some(Gesture::Brush { points, pressure }) = &app.gesture {
|
||||
let screen: Vec<Pos2> = points.iter().map(|q| map.screen(*q)).collect();
|
||||
let opacity = if app.ui.mask_overlay && app.ui.mask_overlay_opacity > 0.0 { app.ui.mask_overlay_opacity / 100.0 } else { 110.0 / 255.0 };
|
||||
// opacity 0 only gets here in the image views, which ignore it: a fixed tint there
|
||||
let opacity = if app.ui.mask_overlay_opacity > 0.0 { app.ui.mask_overlay_opacity / 100.0 } else { 110.0 / 255.0 };
|
||||
let params = crate::brush_live::LiveParams {
|
||||
clip,
|
||||
scale: ui.ctx().pixels_per_point(),
|
||||
|
||||
@@ -1883,6 +1883,114 @@ fn a_fast_brush_stroke_shows_without_gaps_before_the_button_is_released() {
|
||||
}
|
||||
}
|
||||
|
||||
/// What the photo shows along the centreline of [`hold_fast_stroke`]'s stroke at height `y`, away
|
||||
/// from the cursor ring at its end.
|
||||
fn stroke_centreline(h: &Headless, y: f32) -> Vec<[f32; 3]> {
|
||||
let im = h.paint();
|
||||
let img = h.app.image_rect.expect("the photo is on screen");
|
||||
let ppp = h.pixels_per_point;
|
||||
(1..=74)
|
||||
.map(|i| {
|
||||
let x = 0.17 + (0.85 - 0.17) * i as f32 / 80.0;
|
||||
let (sx, sy) = (((img.left() + x * img.width()) * ppp) as usize, ((img.top() + y * img.height()) * ppp) as usize);
|
||||
let c = im.pixels[sy * im.size[0] + sx];
|
||||
[c.r() as f32, c.g() as f32, c.b() as f32]
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// The fast stroke of the #517 test at height `y`, with the button still held: its centreline
|
||||
/// before painting and while painting.
|
||||
fn hold_fast_stroke(h: &mut Headless, y: f32) -> (Vec<[f32; 3]>, Vec<[f32; 3]>) {
|
||||
pointer(h, json!([{"kind": "move", "x": 0.02, "y": 0.02}]));
|
||||
h.settle(SETTLE);
|
||||
let photo = stroke_centreline(h, y);
|
||||
pointer(
|
||||
h,
|
||||
json!([{"kind": "down", "x": 0.15, "y": y}, {"kind": "drag", "x": 0.17, "y": y}, {"kind": "drag", "x": 0.5, "y": y}, {"kind": "drag", "x": 0.85, "y": y}]),
|
||||
);
|
||||
assert!(matches!(h.app.gesture, Some(crate::panels::detail::Gesture::Brush { .. })), "still painting");
|
||||
(photo, stroke_centreline(h, y))
|
||||
}
|
||||
|
||||
/// The centreline points (photo, held) where the stroke being painted changes the photo.
|
||||
fn tinted(photo: &[[f32; 3]], held: &[[f32; 3]]) -> Vec<([f32; 3], [f32; 3])> {
|
||||
photo.iter().zip(held).filter(|(p, l)| (0..3).any(|k| (p[k] - l[k]).abs() > 6.0)).map(|(p, l)| (*p, *l)).collect()
|
||||
}
|
||||
|
||||
/// Issue #2549: with the mask overlay hidden (a mask slider hid it, or O) or a colour overlay at
|
||||
/// opacity 0, the stroke being painted doesn't tint the photo either, so dodging and burning is
|
||||
/// judged on the photo; the effect shows once the stroke is committed. The all-masks overlay still
|
||||
/// shows it, as it will the committed stroke.
|
||||
#[test]
|
||||
fn a_brush_stroke_is_not_tinted_while_the_mask_overlay_is_hidden() {
|
||||
let mut h = detail("panel.masking");
|
||||
h.settle(SETTLE);
|
||||
exec(&mut h, "tool.brush", json!({"new": true}));
|
||||
let r = h.request("ui.set", json!({"brushSize": 0.03, "brushFeather": 50.0, "brushFlow": 100.0}), T);
|
||||
assert_eq!(r["ok"], true, "{r}");
|
||||
// dodging: brighten the new, still empty mask first (which hides the overlay), then paint
|
||||
exec(&mut h, "mask.adjust", json!({"values": {"exposure": 1.0}}));
|
||||
assert!(!h.app.ui.mask_overlay, "a mask slider hides the overlay");
|
||||
let (photo, held) = hold_fast_stroke(&mut h, 0.3);
|
||||
let t = tinted(&photo, &held);
|
||||
assert!(t.is_empty(), "the hidden overlay tints the stroke being painted: {} of 74 points, first {:?}", t.len(), t.first());
|
||||
assert!(h.app.brush_live.is_none(), "nothing is drawn over the photo while painting");
|
||||
pointer(&mut h, json!([{"kind": "up", "x": 0.85, "y": 0.3}]));
|
||||
let d = develop(&h);
|
||||
let MaskShape::Brush { strokes } = &d.masks[0].components[0].shape else { panic!("brush") };
|
||||
assert!(strokes[0].points.len() >= 3, "{strokes:?}");
|
||||
assert!(!h.app.ui.mask_overlay, "painting leaves the overlay hidden");
|
||||
// committed: the stroke brightens the photo
|
||||
h.settle(SETTLE);
|
||||
let luma = |c: &[[f32; 3]]| c.iter().map(|p| p[0] + p[1] + p[2]).sum::<f32>() / c.len() as f32;
|
||||
let done = stroke_centreline(&h, 0.3);
|
||||
assert!(luma(&done) > luma(&photo) + 30.0, "committed {} vs before {}", luma(&done), luma(&photo));
|
||||
|
||||
// O shows the overlay again, but a colour overlay at opacity 0 shows nothing of the committed
|
||||
// strokes, nor of the one being painted
|
||||
let r = h.request("ui.key", json!({"key": "o"}), T);
|
||||
assert_eq!(r["ok"], true, "{r}");
|
||||
assert!(h.app.ui.mask_overlay, "O shows the overlay");
|
||||
exec(&mut h, "view.maskOverlayColor", json!({"opacity": 0}));
|
||||
let (photo, held) = hold_fast_stroke(&mut h, 0.45);
|
||||
let t = tinted(&photo, &held);
|
||||
assert!(t.is_empty(), "an overlay at opacity 0 tints the stroke being painted: {} of 74 points, first {:?}", t.len(), t.first());
|
||||
pointer(&mut h, json!([{"kind": "up", "x": 0.85, "y": 0.45}]));
|
||||
|
||||
// O hides it again, at its usual opacity
|
||||
exec(&mut h, "view.maskOverlayColor", json!({"opacity": 50}));
|
||||
let r = h.request("ui.key", json!({"key": "o"}), T);
|
||||
assert_eq!(r["ok"], true, "{r}");
|
||||
assert!(!h.app.ui.mask_overlay, "O hides the overlay");
|
||||
let (photo, held) = hold_fast_stroke(&mut h, 0.6);
|
||||
let t = tinted(&photo, &held);
|
||||
assert!(t.is_empty(), "the overlay hidden with O tints the stroke being painted: {} of 74 points, first {:?}", t.len(), t.first());
|
||||
pointer(&mut h, json!([{"kind": "up", "x": 0.85, "y": 0.6}]));
|
||||
|
||||
// the all-masks overlay shows every mask's region, so the stroke being painted is tinted as it
|
||||
// will be once committed
|
||||
exec(&mut h, "view.maskOverlayAll", json!({}));
|
||||
assert!(h.app.ui.mask_overlay_all && !h.app.ui.mask_overlay);
|
||||
let (photo, held) = hold_fast_stroke(&mut h, 0.75);
|
||||
let [r0, g0, b0] = h.app.ui.mask_overlay_color.map(|v| v as f32);
|
||||
let op = h.app.ui.mask_overlay_opacity / 100.0;
|
||||
for (p, got) in photo.iter().zip(&held) {
|
||||
let want = [p[0] + (r0 - p[0]) * op, p[1] + (g0 - p[1]) * op, p[2] + (b0 - p[2]) * op];
|
||||
assert!((0..3).all(|k| (got[k] - want[k]).abs() <= 12.0), "the all-masks overlay tints the stroke: {got:?} vs {want:?}");
|
||||
}
|
||||
pointer(&mut h, json!([{"kind": "up", "x": 0.85, "y": 0.75}]));
|
||||
assert_eq!(develop(&h).masks.len(), 1, "every stroke went into the one mask");
|
||||
|
||||
// a hidden mask isn't in the all-masks overlay: nor is the stroke being painted into it
|
||||
let id = develop(&h).masks[0].id;
|
||||
exec(&mut h, "mask.visible", json!({"id": id, "visible": false}));
|
||||
let (photo, held) = hold_fast_stroke(&mut h, 0.9);
|
||||
let t = tinted(&photo, &held);
|
||||
assert!(t.is_empty(), "the all-masks overlay tints a stroke into a hidden mask: {} of 74 points, first {:?}", t.len(), t.first());
|
||||
pointer(&mut h, json!([{"kind": "up", "x": 0.85, "y": 0.9}]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn effect_dropdown_applies_saves_and_shows_the_matching_effect() {
|
||||
let mut h = detail("panel.masking");
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Lightroom parity checklist
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-27 · **Change:** minor (LR-FILT-SEARCH-META: `>=` / `<=` in fielded search tokens; MENU-FILE-CLOSE, KEY-CLOSEWINDOW: the notes say Close Window quits the app, after the same check as Quit; MENU-EDIT-SELECTNONE, MENU-EDIT-PASTESELECTED: Edit lists Select Only Active Photo after Deselect All and Paste Settings from Previous after Paste Selected Settings…; on macOS no doubled separators are left where Settings… moved to the app menu; LR-BEHAV-TITLEBAR: a double-click follows the system setting, and a panel over the bar keeps the window still; KEYC-METADATA, LRC-LIB-KEYWORDS: ⌥0 / ⌥⇧0 go to the next / previous keyword set; LRC-DEV-HISTOGRAM-DRAG: the histogram's five tonal zones — hover for the value, drag to adjust the Basic control the press locked, double-click to reset; LR-VIEW-HISTOGRAM note updated; MENU-BAR: on macOS a menu item that yields its key to a focused text field or to shortcut recording really lets it go; LR-MERGE-HDR / LR-MERGE-PANO: Lanczos-3 warps; LR-IMP-LOCAL: browsing from the sidebar records the folder; LR-VIEW-ZOOM: only a full-size drag within 20 ms clears a slow-drag verdict; LR-IMP-ADD-DIALOG: hierarchical import album destination reconciled with main, #2362; KEY-SLIDER-NUDGE: vertical wheel nudges need a click or drag in the row; leaving it disarms them; LR-EDIT-OPTICS-PROFILE: Fujifilm nine-knot lens tables read at the plain radius; LR-VIEW-ZOOM: only a full-size drag within 20 ms clears a slow-drag verdict; LRC-LIB-LIBFOLDERS, LR-BEHAV-BG-TASKS: Synchronize Folder's scan shows its folder listing and stops it on ✕; LRC-LIB-LIBFOLDERS: Synchronize Folder says a folder is up to date; LR-LIB-STACK: filmstrip stack badge click; LRC-LIB-REFVIEW / CATALOG-SWITCH: Escape preserves edits; catalog-local pins; deleted and malformed references refused; LRC-LIB-COMPARE: the Info panel labels the active photo while culling, #2398; KEYC-RATING: `\` shows / hides the Filter Bar in the grids; LR-VIEW-PHOTOGRID: the grid reads only the photos an import batch or an edit touched, not the whole list; earlier: LRC-PRINT-PAGE: Print Page Options; LR-META-XMP: fewer false unimported-XMP warnings; KEYC-METADATA: ⌘K / Ctrl+K Add Keywords; LRC-DEV-HISTOGRAM-DRAG: the histogram's five tonal zones — hover for the value, drag to adjust the Basic control the press locked, double-click to reset; LR-VIEW-HISTOGRAM note updated; MENU-BAR: on macOS a menu item that yields its key to a focused text field or to shortcut recording really lets it go; LR-MERGE-HDR / LR-MERGE-PANO: Lanczos-3 warps; LR-IMP-LOCAL: browsing from the sidebar records the folder; LR-VIEW-ZOOM: only a full-size drag within 20 ms clears a slow-drag verdict; LR-IMP-ADD-DIALOG: hierarchical import album destination reconciled with main, #2362; KEY-SLIDER-NUDGE: vertical wheel nudges need a click or drag in the row; leaving it disarms them; LR-EDIT-OPTICS-PROFILE: Fujifilm nine-knot lens tables read at the plain radius; LR-VIEW-ZOOM: only a full-size drag within 20 ms clears a slow-drag verdict; LRC-LIB-LIBFOLDERS, LR-BEHAV-BG-TASKS: Synchronize Folder's scan shows its folder listing and stops it on ✕; LRC-LIB-LIBFOLDERS: Synchronize Folder says a folder is up to date; LR-LIB-STACK: filmstrip stack badge click; LRC-LIB-REFVIEW / CATALOG-SWITCH: Escape preserves edits; catalog-local pins; deleted and malformed references refused; LRC-LIB-COMPARE: the Info panel labels the active photo while culling, #2398; KEYC-RATING: `\` shows / hides the Filter Bar in the grids; LR-VIEW-PHOTOGRID: the grid reads only the photos an import batch or an edit touched, not the whole list; earlier: LRC-PRINT-PAGE: Print Page Options; LR-META-XMP: fewer false unimported-XMP warnings; LR-MASK-BRUSH: extreme brush segments skip the intermediate dabs) · **Target:** Adobe Lightroom Classic 15.5 / Lightroom 9.6
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-27 · **Change:** minor (LR-FILT-SEARCH-META: `>=` / `<=` in fielded search tokens; MENU-FILE-CLOSE, KEY-CLOSEWINDOW: the notes say Close Window quits the app, after the same check as Quit; MENU-EDIT-SELECTNONE, MENU-EDIT-PASTESELECTED: Edit lists Select Only Active Photo after Deselect All and Paste Settings from Previous after Paste Selected Settings…; on macOS no doubled separators are left where Settings… moved to the app menu; LR-BEHAV-TITLEBAR: a double-click follows the system setting, and a panel over the bar keeps the window still; KEYC-METADATA, LRC-LIB-KEYWORDS: ⌥0 / ⌥⇧0 go to the next / previous keyword set; LRC-DEV-HISTOGRAM-DRAG: the histogram's five tonal zones — hover for the value, drag to adjust the Basic control the press locked, double-click to reset; LR-VIEW-HISTOGRAM note updated; MENU-BAR: on macOS a menu item that yields its key to a focused text field or to shortcut recording really lets it go; LR-MERGE-HDR / LR-MERGE-PANO: Lanczos-3 warps; LR-IMP-LOCAL: browsing from the sidebar records the folder; LR-VIEW-ZOOM: only a full-size drag within 20 ms clears a slow-drag verdict; LR-IMP-ADD-DIALOG: hierarchical import album destination reconciled with main, #2362; KEY-SLIDER-NUDGE: vertical wheel nudges need a click or drag in the row; leaving it disarms them; LR-EDIT-OPTICS-PROFILE: Fujifilm nine-knot lens tables read at the plain radius; LR-VIEW-ZOOM: only a full-size drag within 20 ms clears a slow-drag verdict; LRC-LIB-LIBFOLDERS, LR-BEHAV-BG-TASKS: Synchronize Folder's scan shows its folder listing and stops it on ✕; LRC-LIB-LIBFOLDERS: Synchronize Folder says a folder is up to date; LR-LIB-STACK: filmstrip stack badge click; LRC-LIB-REFVIEW / CATALOG-SWITCH: Escape preserves edits; catalog-local pins; deleted and malformed references refused; LRC-LIB-COMPARE: the Info panel labels the active photo while culling, #2398; KEYC-RATING: `\` shows / hides the Filter Bar in the grids; LR-VIEW-PHOTOGRID: the grid reads only the photos an import batch or an edit touched, not the whole list; earlier: LRC-PRINT-PAGE: Print Page Options; LR-META-XMP: fewer false unimported-XMP warnings; KEYC-METADATA: ⌘K / Ctrl+K Add Keywords; LRC-DEV-HISTOGRAM-DRAG: the histogram's five tonal zones — hover for the value, drag to adjust the Basic control the press locked, double-click to reset; LR-VIEW-HISTOGRAM note updated; MENU-BAR: on macOS a menu item that yields its key to a focused text field or to shortcut recording really lets it go; LR-MERGE-HDR / LR-MERGE-PANO: Lanczos-3 warps; LR-IMP-LOCAL: browsing from the sidebar records the folder; LR-VIEW-ZOOM: only a full-size drag within 20 ms clears a slow-drag verdict; LR-IMP-ADD-DIALOG: hierarchical import album destination reconciled with main, #2362; KEY-SLIDER-NUDGE: vertical wheel nudges need a click or drag in the row; leaving it disarms them; LR-EDIT-OPTICS-PROFILE: Fujifilm nine-knot lens tables read at the plain radius; LR-VIEW-ZOOM: only a full-size drag within 20 ms clears a slow-drag verdict; LRC-LIB-LIBFOLDERS, LR-BEHAV-BG-TASKS: Synchronize Folder's scan shows its folder listing and stops it on ✕; LRC-LIB-LIBFOLDERS: Synchronize Folder says a folder is up to date; LR-LIB-STACK: filmstrip stack badge click; LRC-LIB-REFVIEW / CATALOG-SWITCH: Escape preserves edits; catalog-local pins; deleted and malformed references refused; LRC-LIB-COMPARE: the Info panel labels the active photo while culling, #2398; KEYC-RATING: `\` shows / hides the Filter Bar in the grids; LR-VIEW-PHOTOGRID: the grid reads only the photos an import batch or an edit touched, not the whole list; earlier: LRC-PRINT-PAGE: Print Page Options; LR-META-XMP: fewer false unimported-XMP warnings; LR-MASK-BRUSH: extreme brush segments skip the intermediate dabs; LR-MASK-BRUSH: with the mask overlay hidden, the stroke being painted no longer tints the photo) · **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
|
||||
@@ -296,7 +296,7 @@ differences and missing bindings are in [ui-parity.md](ui-parity.md#shortcuts).
|
||||
| LR-MASK-OBJECTS | Object selection | P2 | 🟡 | `cmd:mask.add` (`object`, `prompt`), `cmd:mask.objectPoint`, `cmd:mask.refineDetail`, `cmd:segment.prepare`, `cmd:segment.model.status`, `cmd:segment.model.download`, `cmd:segment.model.cancel`, `cmd:app.openSamModelFolder`, `crates/segment`, `crates/engine/src/segment.rs` | SAM 3 in pure Rust (candle; Metal on macOS, CPU elsewhere): Object tile → click to include, ⌥-click to leave out; Describe tile → a text prompt selects every instance ("sky", "the red car"); both also as Add/Subtract/Intersect components; + / − under the selected mask; comma lists (`car, road`); a zoomed-in detail pass for 5–10× finer edges on small objects; per-selection Edge (hard ↔ soft). The model runs on its own worker thread (the UI never waits; panics become errors; a job that fails on the Mac's GPU runs again on the CPU, which then serves the rest of the session, #1974), is unloaded after 10 min idle, and is optional: the segmentation is stored with the mask (288² logits), so renders and exports never need it. Weights are never bundled (SAM License): without them the app offers a consented background download (mirrors, resume, timeouts, SHA-256) — **but no default download location is configured yet** (users set `LIGHTCRAFT_SAM3_MIRRORS` or install by hand: the dialog names the three files, opens the configured model folder with the host opener, preserves setup on an open error, and links docs/ai-masks.md). Not verified against Lightroom's Select Object; no brush/box object mode; the first click on a photo waits for its analysis (~4 s on an M4 Pro, much longer on CPU) |
|
||||
| LR-MASK-PEOPLE | People parts | P2 | ⬜ | | shape exists (no UI): uses the photo's person / skin / hair / teeth / glasses mattes when it carries them (DNG semantic masks), else the subject heuristic |
|
||||
| LR-MASK-LANDSCAPE | Landscape classes | P2 | ⬜ | | shape exists, evaluates empty |
|
||||
| LR-MASK-BRUSH | Brush mask | P0 | 🟡 | `cmd:tool.brush`, `cmd:mask.brushStroke` (`autoMask`), `crates/pipeline/src/masks.rs`, `crates/ui-egui/src/brush_live.rs` | size/feather/flow/density/erase; Create New Mask starts a separate brush in paint mode; the stroke shows while the button is held with the committed stroke's dab spacing, feather and flow, so fast strokes have no gaps (#517); coverage builds up by passes, not hold time (holding the brush still adds nothing, live or committed, #518); oversized path segments skip intermediate dabs instead of allocating a million samples; Auto Mask: dabs weighted by similarity to the colour under the dab centre, refined by a guided filter on luminance (CPU + GPU); ⇧-click draws a straight stroke from where the last stroke ended (`connect`), tapering size, feather and flow from that stroke's values when they changed; no A/B brushes; pen pressure (Brush ▸ Pen Pressure, on by default; `pressure` per point in `mask.brushStroke`) scales each dab's size (a tenth to all of it) and flow, live and committed alike, where winit reports a pen force (Windows; not macOS / Linux tablets yet); no tilt |
|
||||
| LR-MASK-BRUSH | Brush mask | P0 | 🟡 | `cmd:tool.brush`, `cmd:mask.brushStroke` (`autoMask`), `crates/pipeline/src/masks.rs`, `crates/ui-egui/src/brush_live.rs` | size/feather/flow/density/erase; Create New Mask starts a separate brush in paint mode; the stroke shows while the button is held with the committed stroke's dab spacing, feather and flow, so fast strokes have no gaps (#517); with the mask overlay hidden (O / the eye, or after a mask slider hid it) or a colour overlay at opacity 0 it is not tinted while painting either, so dodging and burning is judged on the photo, and its effect shows once the stroke is committed (#2549); coverage builds up by passes, not hold time (holding the brush still adds nothing, live or committed, #518); oversized path segments skip intermediate dabs instead of allocating a million samples; Auto Mask: dabs weighted by similarity to the colour under the dab centre, refined by a guided filter on luminance (CPU + GPU); ⇧-click draws a straight stroke from where the last stroke ended (`connect`), tapering size, feather and flow from that stroke's values when they changed; no A/B brushes; pen pressure (Brush ▸ Pen Pressure, on by default; `pressure` per point in `mask.brushStroke`) scales each dab's size (a tenth to all of it) and flow, live and committed alike, where winit reports a pen force (Windows; not macOS / Linux tablets yet); no tilt |
|
||||
| LR-MASK-LINEAR | Linear gradient | P0 | ✅ | `cmd:tool.linear`, `cmd:mask.update` | |
|
||||
| LR-MASK-RADIAL | Radial gradient | P0 | ✅ | `cmd:tool.radial`, `cmd:mask.update`, `crates/ui-egui/src/panels/detail.rs` | drag the selected ellipse's body or centre pin to move; edge grips resize, outer grip rotates; rotated and compound radial components, hidden pins, outside hit rejection and one-step undo tested |
|
||||
| LR-MASK-COLORRANGE | Colour range | P1 | ✅ | `cmd:mask.add` (`colorRange`), `cmd:mask.sampleColor`, `cmd:mask.update`, `crates/pipeline/src/lib.rs` (`color_range_sample`) | Color tile → click the photo to sample (⇧-click adds, up to 5; samples taken in the space the mask compares in, respecting section eyes and the requested HDR/SDR rendition), Pick button, Refine slider |
|
||||
@@ -807,6 +807,7 @@ Latin stays upright. Advanced Japanese composition remains open. Regression cove
|
||||
| 2026-10-23 | patch | Export integration: Integrated main 5ca49b3e, retaining Width & Height resizing, export format/bit-depth validation and batch panic cancellation alongside the existing watermark and HDR JPEG size-limit fixes. No feature-count changes. |
|
||||
| 2026-10-23 | minor | LRC-LIB-CATALOG-SWITCH, LR-PREF-GENERAL: Settings ▸ General ▸ At launch (Open the last library / Ask me) with a launch window listing the recent libraries, Alt/Option at launch asks once, File ▸ Open Recent Library ▸ Remove from List and `app.recentLibraries {remove}` (issue #1290); photos dropped while no library is open wait for the one opened (presets are refused then); status and counts unchanged (✅) |
|
||||
| 2026-10-23 | minor | LRC-LIB-CATALOG-IMPORT: Import Lightroom Catalog… (and Open Library's offer for a Lightroom catalog) asks whether the catalog merges into the open library or goes into a new library made next to it; Settings ▸ General or "Don't ask again" switches the question off; `file.importLightroom` takes `into` / `name` / `location` (#2672); status unchanged (🟡) |
|
||||
| 2026-10-22 | patch | LR-MASK-BRUSH: the stroke being painted follows Show Mask Overlay: hidden (or a colour overlay at opacity 0), it no longer tints the photo (#2549); status unchanged (🟡) |
|
||||
| 2026-10-22 | minor | LR-VIEW-INFOOVERLAY: the info overlay shows the star rating and flag, and in the full-screen preview on macOS it starts below the window controls (issue #2460); status unchanged |
|
||||
| 2026-10-22 | patch | KEY-GRID: both grid rows in View show the cycle shortcut and follow keymap changes; direct bindings and native accelerator ownership are preserved (#2500); status and counts unchanged |
|
||||
| 2026-10-22 | minor | LRC-PRINT-LAYOUT: the Japanese photo and postcard paper sizes (L判, 2L判, KG判, はがき, 往復はがき, 六切, 四切, 名刺) are selectable in the page setup and Print dialog and print to PDF at their metric sizes |
|
||||
|
||||
@@ -95,6 +95,12 @@
|
||||
|
||||
## 2026-10-22
|
||||
|
||||
### Dodge and burn with the brush on the photo itself
|
||||
- With the mask overlay hidden (O, View ▸ Show Mask Overlay or the eye beside Masks, or after a mask slider hid it),
|
||||
the brush stroke you are painting is no longer drawn in the overlay colour while you hold the button, so the photo
|
||||
stays visible as you dodge and burn (issue #2549). The stroke's effect still appears when you let go. With the
|
||||
overlay shown, the stroke is tinted as before; a colour overlay at opacity 0 leaves it untinted too.
|
||||
|
||||
### Reliability
|
||||
- Opening a library no longer writes through a link left at the lock's owner note (`catalog.lock.owner`): the link
|
||||
is removed and a new note made in its place, so the file it pointed to is never changed (as for the catalog's
|
||||
|
||||
Reference in New Issue
Block a user