From 8e61ad6b13d94c743ecd35649c2150e596c97bee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=A0=91=E6=B4=8B=20=E9=99=88?= <205215139@qq.com> Date: Sun, 11 Oct 2026 15:56:17 +0800 Subject: [PATCH 1/5] Add Ctrl+O / Cmd+O to show every mask overlay at once Previously the masking overlay showed only the selected mask's region (toggled by O). This adds a new Overlay::AllMasks variant that paints the evaluated alpha of every visible mask that has components in one tint, wired to a new "Show All Mask Overlays" command (Ctrl+O on Windows/Linux, Cmd+O on macOS) reachable from the View menu, the masking panel header (stack icon), and the keyboard shortcut. Both the CPU and GPU render paths evaluate all visible masks and hand their alpha planes to visualize::apply, which layers each mask's region so multiple masks are visible at once. The all-masks overlay takes precedence over the single-mask overlay so the shortcut is not shadowed by the always-on per-mask view. --- crates/gpu/src/render.rs | 12 +++++- crates/pipeline/src/lib.rs | 21 ++++++++++- crates/pipeline/src/visualize.rs | 56 +++++++++++++++++++++++++++- crates/ui-egui/src/menubar.rs | 2 + crates/ui-egui/src/menus.rs | 6 +++ crates/ui-egui/src/panels/detail.rs | 12 ++++++ crates/ui-egui/src/panels/masking.rs | 4 ++ crates/ui-egui/src/state.rs | 4 ++ crates/ui-egui/src/tests_masking.rs | 9 +++++ 9 files changed, 122 insertions(+), 4 deletions(-) diff --git a/crates/gpu/src/render.rs b/crates/gpu/src/render.rs index 45a5a5f6..1ace4807 100644 --- a/crates/gpu/src/render.rs +++ b/crates/gpu/src/render.rs @@ -688,6 +688,16 @@ pub fn render( _ => Err(m), } }); + // every visible mask's alpha, for `Overlay::AllMasks` (one tint over all masks at once) + let all_overlay_alphas: Option> = if matches!(req.overlay, lightcraft_pipeline::Overlay::AllMasks { .. }) { + let list: Vec<_> = s.masks.iter().filter(|m| m.visible && !m.components.is_empty()).collect(); + masks.as_ref().map(|all| { + list.iter().enumerate().map(|(mi, _)| { + let b = cx.slice(all, mi * n, n); + cx.read_plane(&b, w, h) + }).collect() + }) + } else { None }; let data: Vec<[u8; 4]> = cx.read(&out, n); let image = Rgba8 { width: w, height: h, data }; lap("finish + readback", &mut t, &mut cx); @@ -723,7 +733,7 @@ pub fn render( lightcraft_pipeline::masks::evaluate_one(m, &plan.frame, w, h, &img, &l, s.light.exposure as f32, plan.mattes.as_deref()) } }); - lightcraft_pipeline::visualize::apply(&mut image, req.overlay, &plan, overlay_mask.as_ref()); + lightcraft_pipeline::visualize::apply(&mut image, req.overlay, &plan, overlay_mask.as_ref(), all_overlay_alphas.as_deref()); // spots grew the window the render worked on: cut it back to the request if let Some(k) = plan.keep { image = image.crop(k.x, k.y, k.w, k.h); diff --git a/crates/pipeline/src/lib.rs b/crates/pipeline/src/lib.rs index f7c33ee9..82d05800 100644 --- a/crates/pipeline/src/lib.rs +++ b/crates/pipeline/src/lib.rs @@ -551,7 +551,8 @@ fn render_impl(src: Src<'_>, info: &SourceInfo, s: &DevelopSettings, req: &Rende lap("histogram", &mut t); let mut image = image; let mask = overlay_alpha(req.overlay, &plan, &prep); - visualize::apply(&mut image, req.overlay, &plan, mask.as_ref()); + let masks = overlay_alphas_all(req.overlay, &plan, &prep); + visualize::apply(&mut image, req.overlay, &plan, mask.as_ref(), masks.as_deref()); let mut image = if plan.keep.is_some() { cut(&image) } else { image }; if req.overlay == Overlay::HdrRange && edit.hdr.enabled { // the HDR rendition of the same request says how far above SDR white each pixel goes @@ -603,6 +604,24 @@ fn overlay_alpha(o: Overlay, plan: &Plan<'_>, prep: &Prepared) -> Option Some(masks::evaluate_one(m, &plan.frame, plan.w, plan.h, &prep.img, &prep.log_l, ev, plan.mattes.as_deref())) } +/// The alpha planes an `Overlay::AllMasks` shows: one per visible mask that has components, +/// reusing the render's own evaluation where available (a fresh evaluation otherwise). +fn overlay_alphas_all(o: Overlay, plan: &Plan<'_>, prep: &Prepared) -> Option> { + if !matches!(o, Overlay::AllMasks { .. }) { + return None; + } + let ev = plan.settings.light.exposure as f32; + let mut out = Vec::new(); + for m in plan.settings.masks.iter().filter(|m| m.visible && !m.components.is_empty()) { + if let Some(e) = prep.masks.iter().find(|e| e.id == m.id) { + out.push(e.alpha.clone()); + } else { + out.push(masks::evaluate_one(m, &plan.frame, plan.w, plan.h, &prep.img, &prep.log_l, ev, plan.mattes.as_deref())); + } + } + Some(out) +} + /// Convenience: render a before/after pair side by side is up to the UI; this renders "before" /// (default look, keeping the crop so framing matches, under the photo's own rendering process). pub fn before_settings(s: &DevelopSettings) -> DevelopSettings { diff --git a/crates/pipeline/src/visualize.rs b/crates/pipeline/src/visualize.rs index fc3d8390..dd87ce33 100644 --- a/crates/pipeline/src/visualize.rs +++ b/crates/pipeline/src/visualize.rs @@ -33,6 +33,9 @@ pub enum Overlay { /// The evaluated alpha of mask `id` (a [`lightcraft_develop::Mask`] id), drawn as `view` in /// `color` at `opacity` (0..100; the colour views only). Mask { id: u16, view: MaskView, color: [u8; 3], opacity: u8 }, + /// The evaluated alpha of every visible mask that has components, drawn as one `view` in + /// `color` at `opacity` (0..100; the colour views only) — every mask's region at once. + AllMasks { view: MaskView, color: [u8; 3], opacity: u8 }, /// Visualize HDR (photos edited in HDR): the SDR image dimmed to grey, with every pixel the HDR /// rendition puts above SDR white painted by how far above it is ([`HDR_BANDS`]). HdrRange, @@ -120,6 +123,7 @@ impl Overlay { Overlay::PointColorRange(i) => (1, i as f64), Overlay::Spots(t) => (2, t as f64), Overlay::Mask { id, view, color, opacity } => (3, pack_mask(id, view, color, opacity) as f64), + Overlay::AllMasks { view, color, opacity } => (5, pack_mask(0, view, color, opacity) as f64), Overlay::HdrRange => (4, 0.0), } } @@ -131,6 +135,14 @@ impl Overlay { 2 => Overlay::Spots(v.clamp(0.0, 100.0) as u8), 3 if v.is_finite() && v >= 0.0 => unpack_mask(v as u64), 4 => Overlay::HdrRange, + 5 if v.is_finite() && v >= 0.0 => { + let rgb = (v as u64 >> 20) & 0xff_ffff; + Overlay::AllMasks { + view: MaskView::ALL.get(((v as u64 >> 16) & 0xf) as usize).copied().unwrap_or_default(), + color: [(rgb >> 16) as u8, (rgb >> 8) as u8, rgb as u8], + opacity: ((v as u64 >> 44) & 0x7f).min(100) as u8, + } + } _ => Overlay::None, } } @@ -142,6 +154,7 @@ impl Overlay { Overlay::PointColorRange(i) => 0x1000 + i as u64, Overlay::Spots(t) => 0x2000 + t as u64, Overlay::Mask { id, view, color, opacity } => 3 << 60 | pack_mask(id, view, color, opacity), + Overlay::AllMasks { view, color, opacity } => 5 << 60 | pack_mask(0, view, color, opacity), Overlay::HdrRange => 0x4000, } } @@ -168,8 +181,9 @@ pub fn adjust_settings(o: Overlay, s: &mut Cow<'_, DevelopSettings>) { } /// Draw overlay `o` over `img` (rendered with `plan`). `mask` is the evaluated alpha of the mask -/// an [`Overlay::Mask`] shows, at the image's size (see [`Overlay::mask`]). -pub fn apply(img: &mut Rgba8, o: Overlay, plan: &Plan<'_>, mask: Option<&Plane>) { +/// an [`Overlay::Mask`] shows, at the image's size (see [`Overlay::mask`]); `masks` is the set of +/// alphas an [`Overlay::AllMasks`] shows, one per visible mask with components. +pub fn apply(img: &mut Rgba8, o: Overlay, plan: &Plan<'_>, mask: Option<&Plane>, masks: Option<&[Plane]>) { match o { // drawn by the renderer from the HDR rendition ([`hdr_range`]) Overlay::None | Overlay::HdrRange => {} @@ -184,6 +198,15 @@ pub fn apply(img: &mut Rgba8, o: Overlay, plan: &Plan<'_>, mask: Option<&Plane>) mask_view(img, a, view, color, opacity); } } + Overlay::AllMasks { view, color, opacity } => { + if let Some(planes) = masks { + for a in planes { + if (a.width, a.height) == (img.width, img.height) { + mask_view(img, a, view, color, opacity); + } + } + } + } } } @@ -380,4 +403,33 @@ mod tests { assert_eq!(MaskView::WhiteOnBlack.next(), MaskView::Color); assert_eq!(MaskView::parse("colorOnBw"), Some(MaskView::ColorOnBw)); } + + #[test] + fn all_masks_overlay_shows_every_visible_mask_at_once() { + use lightcraft_develop::{Mask, MaskComponent, MaskOp, MaskShape}; + use lightcraft_geom::Point; + let src = Rgb32f::from_fn(80, 40, |_, _| [0.1, 0.1, 0.1]); + let info = SourceInfo::default(); + let left = MaskShape::Radial { center: Point::new(0.25, 0.5), rx: 0.15, ry: 0.15, angle: 0.0, feather: 10.0, invert: false }; + let right = MaskShape::Radial { center: Point::new(0.75, 0.5), rx: 0.15, ry: 0.15, angle: 0.0, feather: 10.0, invert: false }; + let mut s = DevelopSettings::default(); + s.masks.push(Mask { id: 1, visible: true, components: vec![MaskComponent { name: None, op: MaskOp::Add, invert: false, shape: left }], ..Default::default() }); + s.masks.push(Mask { id: 2, visible: true, components: vec![MaskComponent { name: None, op: MaskOp::Add, invert: false, shape: right }], ..Default::default() }); + let plain = render(&src, &info, &s, &RenderRequest::fit(80, 40)).image; + let o = Overlay::AllMasks { view: MaskView::WhiteOnBlack, color: [255, 0, 0], opacity: 100 }; + let img = render(&src, &info, &s, &RenderRequest { overlay: o, ..RenderRequest::fit(80, 40) }).image; + let (in_l, in_r, mid) = (20 * 80 + 12, 20 * 80 + 60, 20 * 80 + 40); + assert_eq!(img.data[in_l], [255; 4], "left mask region painted"); + assert_eq!(img.data[in_r], [255; 4], "right mask region painted"); + assert_eq!(img.data[mid], [0, 0, 0, 255], "between the masks: black"); + // a hidden mask is skipped + s.masks[1].visible = false; + let img2 = render(&src, &info, &s, &RenderRequest { overlay: o, ..RenderRequest::fit(80, 40) }).image; + assert_eq!(img2.data[in_r], plain.data[in_r], "hidden right mask not painted"); + assert_eq!(img2.data[in_l], [255; 4], "visible left mask still painted"); + // parts round trip and a stable, distinct cache key + let (k, v) = o.to_parts(); + assert_eq!(Overlay::from_parts(k, v), o); + assert_ne!(o.key(), Overlay::Mask { id: 1, view: MaskView::WhiteOnBlack, color: [255, 0, 0], opacity: 100 }.key()); + } } diff --git a/crates/ui-egui/src/menubar.rs b/crates/ui-egui/src/menubar.rs index b919aa2e..89f59d1e 100644 --- a/crates/ui-egui/src/menubar.rs +++ b/crates/ui-egui/src/menubar.rs @@ -149,6 +149,7 @@ const LAYOUT: &[(&str, &[&str])] = &[ "view.visualizeHdr", "view.softProof", "view.maskOverlay", + "view.maskOverlayAll", "view.maskOverlayMode", "view.maskOverlayColor", "view.maskPins", @@ -312,6 +313,7 @@ pub fn checked(app: &LightcraftApp, id: &str) -> Option { "view.visualizeHdr" => Some(u.hdr_visualize), "view.softProof" => Some(u.soft_proof), "view.maskOverlay" => Some(u.mask_overlay), + "view.maskOverlayAll" => Some(u.mask_overlay_all), "view.maskPins" => Some(u.mask_pins), "view.showOriginal" => Some(u.before_after == crate::state::BeforeAfter::Original), "view.beforeAfter" => Some(u.before_after == crate::state::BeforeAfter::SideBySide), diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index 81a46eb9..5ae64e1a 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -91,6 +91,8 @@ pub const UI_COMMANDS: &[UiCommand] = &[ ("view.softProof", "Soft Proofing", Some("S"), "View"), ("view.histogram", "Histogram", Some("Cmd+Shift+H"), "View"), ("view.maskOverlay", "Show Mask Overlay", Some("O"), "View"), + // Ctrl+O / Cmd+O: every mask's region at once, not just the selected one + ("view.maskOverlayAll", "Show All Mask Overlays", Some("Cmd+O"), "View"), // Shift+O in the Masking panel (elsewhere it cycles the crop overlay) ("view.maskOverlayMode", "Cycle Mask Overlay Mode", None, "View"), ("view.maskOverlayColor", "Cycle Mask Overlay Color", None, "View"), @@ -634,6 +636,10 @@ pub fn run_ui_command(app: &mut LightcraftApp, id: &str, p: &Value) -> Option { + app.ui.mask_overlay_all = !app.ui.mask_overlay_all; + Ok(json!({"maskOverlayAll": app.ui.mask_overlay_all})) + } "view.maskOverlayMode" => { use lightcraft_engine::pipeline::MaskView; let cur = MaskView::parse(&app.ui.mask_overlay_mode).unwrap_or_default(); diff --git a/crates/ui-egui/src/panels/detail.rs b/crates/ui-egui/src/panels/detail.rs index c3678ae2..24976d94 100644 --- a/crates/ui-egui/src/panels/detail.rs +++ b/crates/ui-egui/src/panels/detail.rs @@ -1065,6 +1065,18 @@ pub(crate) fn view_overlay(app: &LightcraftApp, d: &DevelopSettings) -> lightcra if app.ui.fullscreen { return Overlay::None; } + // Show every visible mask's region at once (Ctrl+O / Cmd+O), ahead of the selected-mask overlay + // so the shortcut is not shadowed by the always-on single-mask overlay. + if app.ui.right == RightPanel::Masking + && app.ui.mask_overlay_all + && d.masks.iter().any(|m| m.visible && !m.components.is_empty()) + { + return Overlay::AllMasks { + view: MaskView::parse(&app.ui.mask_overlay_mode).unwrap_or_default(), + color: app.ui.mask_overlay_color, + opacity: app.ui.mask_overlay_opacity.clamp(0.0, 100.0).round() as u8, + }; + } if app.ui.right == RightPanel::Masking && app.ui.mask_overlay && let Some(m) = app.session.active_mask.and_then(|id| d.masks.iter().find(|m| m.id == id)) diff --git a/crates/ui-egui/src/panels/masking.rs b/crates/ui-egui/src/panels/masking.rs index 640bc4f1..47b96ee6 100644 --- a/crates/ui-egui/src/panels/masking.rs +++ b/crates/ui-egui/src/panels/masking.rs @@ -197,6 +197,10 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) { if icon_button(ui, "maskOverlay", Icon::Eye, vec2(24.0, 24.0), on, true, "Show overlay (O)").clicked() { app.ui.mask_overlay = !on; } + let all = app.ui.mask_overlay_all; + if icon_button(ui, "maskOverlayAll", Icon::Stack, vec2(24.0, 24.0), all, true, "Show all overlays (Ctrl+O)").clicked() { + app.ui.mask_overlay_all = !all; + } }); }); if d.masks.is_empty() { diff --git a/crates/ui-egui/src/state.rs b/crates/ui-egui/src/state.rs index 6a50e2ba..b52f7ec6 100644 --- a/crates/ui-egui/src/state.rs +++ b/crates/ui-egui/src/state.rs @@ -332,6 +332,9 @@ pub struct UiState { pub mask_overlay_color: [u8; 3], pub mask_overlay_opacity: f32, pub mask_pins: bool, + /// Masking: show every visible mask's region as one overlay at once (Ctrl+O / Cmd+O), + /// instead of just the selected mask's ([`mask_overlay`]). + pub mask_overlay_all: bool, /// The overlay (on, mode) to go back to when Show Luminance Map is turned off. #[serde(skip)] pub luminance_map_restore: Option<(bool, String)>, @@ -813,6 +816,7 @@ impl Default for UiState { mask_overlay_color: [230, 30, 40], mask_overlay_opacity: 50.0, mask_pins: true, + mask_overlay_all: false, crop_overlay: CropOverlay::Thirds, crop_overlay_orient: 0, show_filenames: true, diff --git a/crates/ui-egui/src/tests_masking.rs b/crates/ui-egui/src/tests_masking.rs index b95fa1ab..db009e81 100644 --- a/crates/ui-egui/src/tests_masking.rs +++ b/crates/ui-egui/src/tests_masking.rs @@ -168,6 +168,15 @@ fn mask_adjustments_hide_overlay_without_disabling_the_mask() { assert_eq!(develop(&h).masks[0], mask, "toggling the overlay leaves the mask and its effect unchanged"); assert!(matches!(crate::panels::detail::view_overlay(&h.app, &develop(&h)), Overlay::Mask { .. })); + // Ctrl+O shows every visible mask's region at once (the new command), ahead of the selected + // mask overlay so it is not shadowed by the always-on single-mask overlay. + exec(&mut h, "view.maskOverlayAll", json!({})); + assert!(h.app.ui.mask_overlay_all, "Ctrl+O turns on the all-masks overlay"); + assert!(matches!(crate::panels::detail::view_overlay(&h.app, &develop(&h)), Overlay::AllMasks { .. })); + exec(&mut h, "view.maskOverlayAll", json!({})); + assert!(!h.app.ui.mask_overlay_all, "Ctrl+O toggles the all-masks overlay off"); + assert!(!matches!(crate::panels::detail::view_overlay(&h.app, &develop(&h)), Overlay::AllMasks { .. })); + // Failed edits and global adjustments must not hide the mask overlay. assert!(h.app.run("mask.adjust", json!({"id": 999, "values": {"exposure": 1.0}})).is_err()); assert!(h.app.ui.mask_overlay); From 6fd740ac2db0ce253d34510e73d29d4bb3e0d622 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=A0=91=E6=B4=8B=20=E9=99=88?= <205215139@qq.com> Date: Mon, 12 Oct 2026 11:44:03 +0800 Subject: [PATCH 2/5] Address maintainer review: free shortcut, derived tooltip, fmt, locales - view.maskOverlayAll: Cmd+O clashed with library.open; rebind to Cmd+Shift+O - Masking panel tooltip now derives the key label from the binding (platform aware: Cmd vs Ctrl) instead of hardcoding Ctrl+O; base string localized - gpu/render.rs: rewrite the AllMasks alpha gather as a for-loop so it passes cargo fmt --check (max_width 150) - docs/parity-checklist.md: add LR-MASK-OVERLAYALL row - locales: add 'Show All Mask Overlays' and 'Show all overlays' to de, es, fr, ja, pt-br, ru, uk, zh-hans, zh-hant --- crates/gpu/src/render.rs | 14 +++++++++----- crates/ui-egui/locales/de.json | 2 ++ crates/ui-egui/locales/es.json | 2 ++ crates/ui-egui/locales/fr.json | 2 ++ crates/ui-egui/locales/ja.json | 2 ++ crates/ui-egui/locales/pt-br.json | 2 ++ crates/ui-egui/locales/ru.json | 2 ++ crates/ui-egui/locales/uk.json | 2 ++ crates/ui-egui/locales/zh-hans.json | 2 ++ crates/ui-egui/locales/zh-hant.json | 2 ++ crates/ui-egui/src/menus.rs | 4 ++-- crates/ui-egui/src/panels/detail.rs | 2 +- crates/ui-egui/src/panels/masking.rs | 8 +++++++- crates/ui-egui/src/tests_masking.rs | 6 +++--- docs/parity-checklist.md | 1 + 15 files changed, 41 insertions(+), 12 deletions(-) diff --git a/crates/gpu/src/render.rs b/crates/gpu/src/render.rs index 1ace4807..d0bd3aa4 100644 --- a/crates/gpu/src/render.rs +++ b/crates/gpu/src/render.rs @@ -690,14 +690,18 @@ pub fn render( }); // every visible mask's alpha, for `Overlay::AllMasks` (one tint over all masks at once) let all_overlay_alphas: Option> = if matches!(req.overlay, lightcraft_pipeline::Overlay::AllMasks { .. }) { - let list: Vec<_> = s.masks.iter().filter(|m| m.visible && !m.components.is_empty()).collect(); masks.as_ref().map(|all| { - list.iter().enumerate().map(|(mi, _)| { + let list: Vec<_> = s.masks.iter().filter(|m| m.visible && !m.components.is_empty()).collect(); + let mut alphas = Vec::with_capacity(list.len()); + for (mi, _) in list.iter().enumerate() { let b = cx.slice(all, mi * n, n); - cx.read_plane(&b, w, h) - }).collect() + alphas.push(cx.read_plane(&b, w, h)); + } + alphas }) - } else { None }; + } else { + None + }; let data: Vec<[u8; 4]> = cx.read(&out, n); let image = Rgba8 { width: w, height: h, data }; lap("finish + readback", &mut t, &mut cx); diff --git a/crates/ui-egui/locales/de.json b/crates/ui-egui/locales/de.json index 6520fb3e..ca1013ab 100644 --- a/crates/ui-egui/locales/de.json +++ b/crates/ui-egui/locales/de.json @@ -1376,6 +1376,8 @@ "Show Luminance Map": "Luminanzkarte anzeigen", "Show Mask": "Maske anzeigen", "Show Mask Overlay": "Maskenüberlagerung anzeigen", + "Show All Mask Overlays": "Alle Maskenüberlagerungen anzeigen", + "Show all overlays": "Alle Überlagerungen anzeigen", "Show Mask Pins": "Maskenpins anzeigen", "Show Original": "Original anzeigen", "Show Photo Counts": "Fotoanzahl anzeigen", diff --git a/crates/ui-egui/locales/es.json b/crates/ui-egui/locales/es.json index fc53b639..e379f280 100644 --- a/crates/ui-egui/locales/es.json +++ b/crates/ui-egui/locales/es.json @@ -1323,6 +1323,8 @@ "Show Luminance Map": "Mostrar mapa de luminancia", "Show Mask": "Mostrar máscara", "Show Mask Overlay": "Mostrar superposición de máscara", + "Show All Mask Overlays": "Mostrar todas las superposiciones de máscara", + "Show all overlays": "Mostrar todas las superposiciones", "Show Mask Pins": "Mostrar chinchetas de máscara", "Show Original": "Mostrar original", "Show Photo Counts": "Mostrar recuento de fotos", diff --git a/crates/ui-egui/locales/fr.json b/crates/ui-egui/locales/fr.json index 53509a50..ce7a9473 100644 --- a/crates/ui-egui/locales/fr.json +++ b/crates/ui-egui/locales/fr.json @@ -1313,6 +1313,8 @@ "Show Luminance Map": "Afficher la carte de luminance", "Show Mask": "Afficher le masque", "Show Mask Overlay": "Afficher l'incrustation du masque", + "Show All Mask Overlays": "Afficher toutes les superpositions de masque", + "Show all overlays": "Afficher toutes les superpositions", "Show Mask Pins": "Afficher les épingles de masque", "Show Original": "Afficher l'original", "Show Photo Counts": "Afficher le nombre de photos", diff --git a/crates/ui-egui/locales/ja.json b/crates/ui-egui/locales/ja.json index 2babd30b..b4243e70 100644 --- a/crates/ui-egui/locales/ja.json +++ b/crates/ui-egui/locales/ja.json @@ -1259,6 +1259,8 @@ "Show Luminance Map": "輝度マップを表示", "Show Mask": "マスクを表示", "Show Mask Overlay": "マスクの重ね表示", + "Show All Mask Overlays": "すべてのマスクオーバーレイを表示", + "Show all overlays": "すべてのオーバーレイを表示", "Show Mask Pins": "マスクのピンを表示", "Show Original": "元の写真を表示", "Show Photo Counts": "写真の枚数を表示", diff --git a/crates/ui-egui/locales/pt-br.json b/crates/ui-egui/locales/pt-br.json index dfe47281..63515b41 100644 --- a/crates/ui-egui/locales/pt-br.json +++ b/crates/ui-egui/locales/pt-br.json @@ -1334,6 +1334,8 @@ "Show Luminance Map": "Mostrar mapa de luminância", "Show Mask": "Mostrar máscara", "Show Mask Overlay": "Mostrar sobreposição da máscara", + "Show All Mask Overlays": "Mostrar todas as sobreposições de máscara", + "Show all overlays": "Mostrar todas as sobreposições", "Show Mask Pins": "Mostrar pinos da máscara", "Show Original": "Mostrar original", "Show Photo Counts": "Mostrar contagem de fotos", diff --git a/crates/ui-egui/locales/ru.json b/crates/ui-egui/locales/ru.json index fafb65a1..fc0c7ede 100644 --- a/crates/ui-egui/locales/ru.json +++ b/crates/ui-egui/locales/ru.json @@ -1102,6 +1102,8 @@ "Show Luminance Map": "Показать карту яркости", "Show Mask": "Показать маску", "Show Mask Overlay": "Показать наложение маски", + "Show All Mask Overlays": "Показать все наложения масок", + "Show all overlays": "Показать все наложения", "Show Mask Pins": "Показать булавки масок", "Show Original": "Показать оригинал", "Show Photo Counts": "Показывать количество фото", diff --git a/crates/ui-egui/locales/uk.json b/crates/ui-egui/locales/uk.json index 7ebad5b1..56954b6b 100644 --- a/crates/ui-egui/locales/uk.json +++ b/crates/ui-egui/locales/uk.json @@ -1326,6 +1326,8 @@ "Show Hidden Local Locations": "Показати приховані локальні розташування", "Show Luminance Map": "Показати карту яскравості", "Show Mask Overlay": "Показати накладання маски", + "Show All Mask Overlays": "Показати всі накладення масок", + "Show all overlays": "Показати всі накладення", "Show Mask Pins": "Показати маркери масок", "Show Original": "Показати оригінал", "Show Photo Counts": "Показати кількість фото", diff --git a/crates/ui-egui/locales/zh-hans.json b/crates/ui-egui/locales/zh-hans.json index 8a0e58b8..795dc70a 100644 --- a/crates/ui-egui/locales/zh-hans.json +++ b/crates/ui-egui/locales/zh-hans.json @@ -1321,6 +1321,8 @@ "Show Luminance Map": "显示明亮度图", "Show Mask": "显示蒙版", "Show Mask Overlay": "显示蒙版叠加", + "Show All Mask Overlays": "显示所有蒙版叠加", + "Show all overlays": "显示所有叠加", "Show Mask Pins": "显示蒙版图钉", "Show Original": "显示原图", "Show Photo Counts": "显示照片数量", diff --git a/crates/ui-egui/locales/zh-hant.json b/crates/ui-egui/locales/zh-hant.json index f1cbec3a..c4a8ab1f 100644 --- a/crates/ui-egui/locales/zh-hant.json +++ b/crates/ui-egui/locales/zh-hant.json @@ -1321,6 +1321,8 @@ "Show Luminance Map": "顯示明度圖", "Show Mask": "顯示遮色片", "Show Mask Overlay": "顯示遮色片疊加", + "Show All Mask Overlays": "顯示所有蒙版疊加", + "Show all overlays": "顯示所有疊加", "Show Mask Pins": "顯示遮色片控制點", "Show Original": "顯示原始照片", "Show Photo Counts": "顯示照片數量", diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index 5ae64e1a..5f31025c 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -91,8 +91,8 @@ pub const UI_COMMANDS: &[UiCommand] = &[ ("view.softProof", "Soft Proofing", Some("S"), "View"), ("view.histogram", "Histogram", Some("Cmd+Shift+H"), "View"), ("view.maskOverlay", "Show Mask Overlay", Some("O"), "View"), - // Ctrl+O / Cmd+O: every mask's region at once, not just the selected one - ("view.maskOverlayAll", "Show All Mask Overlays", Some("Cmd+O"), "View"), + // Ctrl+Shift+O / Cmd+Shift+O: every mask's region at once, not just the selected one + ("view.maskOverlayAll", "Show All Mask Overlays", Some("Cmd+Shift+O"), "View"), // Shift+O in the Masking panel (elsewhere it cycles the crop overlay) ("view.maskOverlayMode", "Cycle Mask Overlay Mode", None, "View"), ("view.maskOverlayColor", "Cycle Mask Overlay Color", None, "View"), diff --git a/crates/ui-egui/src/panels/detail.rs b/crates/ui-egui/src/panels/detail.rs index 24976d94..f766fe66 100644 --- a/crates/ui-egui/src/panels/detail.rs +++ b/crates/ui-egui/src/panels/detail.rs @@ -1065,7 +1065,7 @@ pub(crate) fn view_overlay(app: &LightcraftApp, d: &DevelopSettings) -> lightcra if app.ui.fullscreen { return Overlay::None; } - // Show every visible mask's region at once (Ctrl+O / Cmd+O), ahead of the selected-mask overlay + // Show every visible mask's region at once (Ctrl+Shift+O / Cmd+Shift+O), ahead of the selected-mask overlay // so the shortcut is not shadowed by the always-on single-mask overlay. if app.ui.right == RightPanel::Masking && app.ui.mask_overlay_all diff --git a/crates/ui-egui/src/panels/masking.rs b/crates/ui-egui/src/panels/masking.rs index 47b96ee6..4698a165 100644 --- a/crates/ui-egui/src/panels/masking.rs +++ b/crates/ui-egui/src/panels/masking.rs @@ -198,7 +198,13 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) { app.ui.mask_overlay = !on; } let all = app.ui.mask_overlay_all; - if icon_button(ui, "maskOverlayAll", Icon::Stack, vec2(24.0, 24.0), all, true, "Show all overlays (Ctrl+O)").clicked() { + let all_mac = ui.ctx().os() == egui::os::OperatingSystem::Mac; + let all_base = crate::i18n::tr("Show all overlays"); + let all_tip = match crate::shortcuts::shortcut_of(&app.ui.settings.keymap, "view.maskOverlayAll") { + Some(sc) => format!("{all_base} ({})", crate::menubar::shortcut_text(sc, all_mac)), + None => all_base.to_string(), + }; + if icon_button(ui, "maskOverlayAll", Icon::Stack, vec2(24.0, 24.0), all, true, &all_tip).clicked() { app.ui.mask_overlay_all = !all; } }); diff --git a/crates/ui-egui/src/tests_masking.rs b/crates/ui-egui/src/tests_masking.rs index db009e81..3ad52b8e 100644 --- a/crates/ui-egui/src/tests_masking.rs +++ b/crates/ui-egui/src/tests_masking.rs @@ -168,13 +168,13 @@ fn mask_adjustments_hide_overlay_without_disabling_the_mask() { assert_eq!(develop(&h).masks[0], mask, "toggling the overlay leaves the mask and its effect unchanged"); assert!(matches!(crate::panels::detail::view_overlay(&h.app, &develop(&h)), Overlay::Mask { .. })); - // Ctrl+O shows every visible mask's region at once (the new command), ahead of the selected + // Ctrl+Shift+O / Cmd+Shift+O shows every visible mask's region at once (the new command), ahead of the selected // mask overlay so it is not shadowed by the always-on single-mask overlay. exec(&mut h, "view.maskOverlayAll", json!({})); - assert!(h.app.ui.mask_overlay_all, "Ctrl+O turns on the all-masks overlay"); + assert!(h.app.ui.mask_overlay_all, "the all-masks overlay command turns it on"); assert!(matches!(crate::panels::detail::view_overlay(&h.app, &develop(&h)), Overlay::AllMasks { .. })); exec(&mut h, "view.maskOverlayAll", json!({})); - assert!(!h.app.ui.mask_overlay_all, "Ctrl+O toggles the all-masks overlay off"); + assert!(!h.app.ui.mask_overlay_all, "the all-masks overlay command toggles it off"); assert!(!matches!(crate::panels::detail::view_overlay(&h.app, &develop(&h)), Overlay::AllMasks { .. })); // Failed edits and global adjustments must not hide the mask overlay. diff --git a/docs/parity-checklist.md b/docs/parity-checklist.md index 09941108..ec6991c4 100644 --- a/docs/parity-checklist.md +++ b/docs/parity-checklist.md @@ -302,6 +302,7 @@ differences and missing bindings are in [ui-parity.md](ui-parity.md#shortcuts). | LR-MASK-FEATHER-EDGE | Refine mask edges | P2 | ✅ | `cmd:mask.refine`, `crates/pipeline/src/masks.rs` (`evaluate_one`) | Refine Edges slider per mask: guided filter on the photo's luminance snaps soft mask edges to the photo's edges (rendered on the CPU) | | LR-MASK-SLIDERS | Local adjustment sliders | P0 | ✅ | `cmd:mask.adjust`, `crates/pipeline/src/finish.rs` | every slider renders (CPU + GPU), incl. Noise, Moiré, Defringe (negative Defringe has no effect); no local curve or effect presets | | LR-MASK-OVERLAY | Mask overlay | P0 | ✅ | `cmd:view.maskOverlay`, `cmd:view.maskOverlayMode`, `cmd:view.maskOverlayColor`, `crates/pipeline/src/visualize.rs` | rendered alpha of the selected mask (CPU + GPU): colour, colour on B&W, image on black/white, white on black; ⇧O cycles while masking; successful local adjustments (including Amount) hide the overlay until manually shown again with O / the eye; shape/edge edits retain it; no auto-show on hover | +| LR-MASK-OVERLAYALL | Show all mask overlays | P1 | ✅ | `cmd:view.maskOverlayAll`, `crates/ui-egui/src/panels/masking.rs`, `crates/pipeline/src/visualize.rs` | one diagnostic tint over every visible mask at once (CPU + GPU): the masks still stack their adjustments as before — this only paints each mask's region together, so you can see all masked areas at a glance (⌘⇧O / Ctrl+Shift+O) | | LR-MASK-PINS | Pins | P1 | ✅ | `cmd:view.maskPins`, `crates/ui-egui/src/panels/detail.rs` | a pin per radial/linear/brush component: click selects its mask, drag moves it; no pins for range/AI components, no Auto mode | | LR-MASK-UPDATE | Recompute AI masks | P2 | ⬜ | | | | LR-MASK-SYNC | Copy masks to other photos | P1 | ✅ | `cmd:develop.copy` (`groups`) | | From 9fc2e69328cdbeafa32f3eb366fbc312088e97d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=A0=91=E6=B4=8B=20=E9=99=88?= <205215139@qq.com> Date: Mon, 12 Oct 2026 13:18:21 +0800 Subject: [PATCH 3/5] gpu: read all mask overlay alphas in a single GPU->CPU readback For Overlay::AllMasks, the masks buffer already holds each visible mask's alpha back-to-back in the same order the single-mask overlay uses (cx.slice(all, mi*n, n)). Read the whole buffer once with cx.read instead of one synchronous read_plane per mask, then split it into per-mask Planes on the CPU. Same bytes, but the pipeline stalls once instead of N times. --- crates/gpu/src/render.rs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/crates/gpu/src/render.rs b/crates/gpu/src/render.rs index 12c7ec75..828d2ba5 100644 --- a/crates/gpu/src/render.rs +++ b/crates/gpu/src/render.rs @@ -712,14 +712,21 @@ pub fn render( _ => Err(m), } }); - // every visible mask's alpha, for `Overlay::AllMasks` (one tint over all masks at once) + // every visible mask's alpha, for `Overlay::AllMasks` (one tint over all masks at once). + // The masks buffer holds each visible mask's alpha back-to-back in the same `list` order the + // single-mask overlay uses, so read the whole buffer back in ONE GPU->CPU transfer and split it + // on the CPU, instead of one synchronous readback per mask (which stalls the pipeline N times). let all_overlay_alphas: Option> = if matches!(req.overlay, lightcraft_pipeline::Overlay::AllMasks { .. }) { masks.as_ref().map(|all| { let list: Vec<_> = s.masks.iter().filter(|m| m.visible && !m.components.is_empty()).collect(); + if list.is_empty() { + return Vec::new(); + } + let raw: Vec = cx.read(all, n * list.len()); let mut alphas = Vec::with_capacity(list.len()); - for (mi, _) in list.iter().enumerate() { - let b = cx.slice(all, mi * n, n); - alphas.push(cx.read_plane(&b, w, h)); + for mi in 0..list.len() { + let s = mi * n; + alphas.push(Plane { width: w, height: h, data: raw[s..s + n].to_vec() }); } alphas }) From 92a2e721e24aa560517e8f90acb6c8fdc564481b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=A0=91=E6=B4=8B=20=E9=99=88?= <205215139@qq.com> Date: Mon, 12 Oct 2026 13:41:14 +0800 Subject: [PATCH 4/5] ui: fix stale Ctrl+O / Cmd+O in mask_overlay_all doc comment The shortcut moved to Ctrl+Shift+O / Cmd+Shift+O; this doc comment still advertised the old binding. --- crates/ui-egui/src/state.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ui-egui/src/state.rs b/crates/ui-egui/src/state.rs index b46542c1..897ef805 100644 --- a/crates/ui-egui/src/state.rs +++ b/crates/ui-egui/src/state.rs @@ -542,7 +542,7 @@ pub struct UiState { pub mask_overlay_color: [u8; 3], pub mask_overlay_opacity: f32, pub mask_pins: bool, - /// Masking: show every visible mask's region as one overlay at once (Ctrl+O / Cmd+O), + /// Masking: show every visible mask's region as one overlay at once (Ctrl+Shift+O / Cmd+Shift+O), /// instead of just the selected mask's ([`mask_overlay`]). pub mask_overlay_all: bool, /// The overlay (on, mode) to go back to when Show Luminance Map is turned off. From 40b7b9b385259cc7cec5f45af8d84c27ac9002e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=A0=91=E6=B4=8B=20=E9=99=88?= <205215139@qq.com> Date: Mon, 12 Oct 2026 14:04:19 +0800 Subject: [PATCH 5/5] fmt: satisfy rustfmt on AllMasks test and detail panel --- crates/pipeline/src/visualize.rs | 14 ++++++++++++-- crates/ui-egui/src/panels/detail.rs | 5 +---- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/crates/pipeline/src/visualize.rs b/crates/pipeline/src/visualize.rs index dd87ce33..15c164bb 100644 --- a/crates/pipeline/src/visualize.rs +++ b/crates/pipeline/src/visualize.rs @@ -413,8 +413,18 @@ mod tests { let left = MaskShape::Radial { center: Point::new(0.25, 0.5), rx: 0.15, ry: 0.15, angle: 0.0, feather: 10.0, invert: false }; let right = MaskShape::Radial { center: Point::new(0.75, 0.5), rx: 0.15, ry: 0.15, angle: 0.0, feather: 10.0, invert: false }; let mut s = DevelopSettings::default(); - s.masks.push(Mask { id: 1, visible: true, components: vec![MaskComponent { name: None, op: MaskOp::Add, invert: false, shape: left }], ..Default::default() }); - s.masks.push(Mask { id: 2, visible: true, components: vec![MaskComponent { name: None, op: MaskOp::Add, invert: false, shape: right }], ..Default::default() }); + s.masks.push(Mask { + id: 1, + visible: true, + components: vec![MaskComponent { name: None, op: MaskOp::Add, invert: false, shape: left }], + ..Default::default() + }); + s.masks.push(Mask { + id: 2, + visible: true, + components: vec![MaskComponent { name: None, op: MaskOp::Add, invert: false, shape: right }], + ..Default::default() + }); let plain = render(&src, &info, &s, &RenderRequest::fit(80, 40)).image; let o = Overlay::AllMasks { view: MaskView::WhiteOnBlack, color: [255, 0, 0], opacity: 100 }; let img = render(&src, &info, &s, &RenderRequest { overlay: o, ..RenderRequest::fit(80, 40) }).image; diff --git a/crates/ui-egui/src/panels/detail.rs b/crates/ui-egui/src/panels/detail.rs index 4c27ed8a..de77369e 100644 --- a/crates/ui-egui/src/panels/detail.rs +++ b/crates/ui-egui/src/panels/detail.rs @@ -1127,10 +1127,7 @@ pub(crate) fn view_overlay(app: &LightcraftApp, d: &DevelopSettings) -> lightcra } // Show every visible mask's region at once (Ctrl+Shift+O / Cmd+Shift+O), ahead of the selected-mask overlay // so the shortcut is not shadowed by the always-on single-mask overlay. - if app.ui.right == RightPanel::Masking - && app.ui.mask_overlay_all - && d.masks.iter().any(|m| m.visible && !m.components.is_empty()) - { + if app.ui.right == RightPanel::Masking && app.ui.mask_overlay_all && d.masks.iter().any(|m| m.visible && !m.components.is_empty()) { return Overlay::AllMasks { view: MaskView::parse(&app.ui.mask_overlay_mode).unwrap_or_default(), color: app.ui.mask_overlay_color,