From 71748919396d7fb0adb8b96d02eb31dd6fc6bcad Mon Sep 17 00:00:00 2001 From: Bruno-Alumn <171815746+Bruno-Alumn@users.noreply.github.com> Date: Thu, 29 Oct 2026 23:05:20 +0100 Subject: [PATCH] LR-MASK-PINS: with the Object tool on, a click on another mask's pin selects it (#3049) --- crates/ui-egui/src/panels/detail/masks.rs | 34 +++++++++++++++++-- crates/ui-egui/src/tests/masking.rs | 41 +++++++++++++++++++++++ docs/whats-new.md | 3 +- 3 files changed, 74 insertions(+), 4 deletions(-) diff --git a/crates/ui-egui/src/panels/detail/masks.rs b/crates/ui-egui/src/panels/detail/masks.rs index 0ab7bb5ee..c67823db0 100644 --- a/crates/ui-egui/src/panels/detail/masks.rs +++ b/crates/ui-egui/src/panels/detail/masks.rs @@ -356,7 +356,7 @@ pub(super) fn mask_overlay(app: &mut LightcraftApp, ui: &mut egui::Ui, resp: &eg return; } if app.ui.tool == "object" { - object_tool(app, ui, resp, map); + object_tool(app, ui, resp, map, d, &grips); return; } if app.ui.tool == "brush" { @@ -457,14 +457,42 @@ fn object_marks(app: &LightcraftApp, p: &egui::Painter, map: &CanvasMap, d: &Dev } } -/// Object tool: a click includes what's under it, ⌥-click leaves it out. -fn object_tool(app: &mut LightcraftApp, ui: &mut egui::Ui, resp: &egui::Response, map: &CanvasMap) { +/// The mask whose pin is under `q` (within the 10 px [`grip_gestures`] grabs at), not counting +/// the `active` mask's own pins. +fn other_pin_at(grips: &[(u32, usize, u8, Pos2)], active: Option, q: Pos2) -> Option { + grips + .iter() + .filter(|g| Some(g.0) != active && g.2 == 0 && g.3.distance(q) < 10.0) + .min_by(|a, b| a.3.distance(q).total_cmp(&b.3.distance(q))) + .map(|g| g.0) +} + +/// Object tool: a click includes what's under it, ⌥-click leaves it out. A click on another +/// mask's pin selects that mask instead of adding a click to the selected one (issue #3049); +/// the tool then stays on only if the newly selected mask has an Object selection to refine. +fn object_tool( + app: &mut LightcraftApp, + ui: &mut egui::Ui, + resp: &egui::Response, + map: &CanvasMap, + d: &DevelopSettings, + grips: &[(u32, usize, u8, Pos2)], +) { if resp.hovered() { ui.ctx().set_cursor_icon(egui::CursorIcon::Crosshair); } if resp.clicked() && let Some(q) = resp.interact_pointer_pos() { + if let Some(mid) = other_pin_at(grips, app.session.active_mask, q) { + let _ = app.run("mask.select", json!({"id": mid})); + let has_object = + d.masks.iter().find(|m| m.id == mid).is_some_and(|m| m.components.iter().any(|c| matches!(c.shape, MaskShape::Object { .. }))); + if !has_object { + app.ui.tool.clear(); + } + return; + } let n = map.norm(q); let exclude = ui.input(|i| i.modifiers.alt); match app.run_typed("mask.objectPoint", json!({"x": n.x, "y": n.y, "exclude": exclude})) { diff --git a/crates/ui-egui/src/tests/masking.rs b/crates/ui-egui/src/tests/masking.rs index 444096d15..4e7358d3f 100644 --- a/crates/ui-egui/src/tests/masking.rs +++ b/crates/ui-egui/src/tests/masking.rs @@ -408,6 +408,47 @@ fn object_and_describe_masks_show_pins_that_select_but_do_not_move() { h.settle(SETTLE); } +/// Issue #3049: with the Object tool on, a click on another mask's pin selects that mask instead +/// of adding an Object click to the selected one. The tool stays on for an Object mask (its +/// clicks refine it) and goes off for a mask with no Object selection to refine. +#[test] +fn the_object_tool_selects_the_mask_whose_pin_is_clicked() { + use lightcraft_geom::Point; + let mut h = detail("panel.masking"); + // no SAM 3 model here: a stray Object click would offer the download or show an error + let dir = std::env::temp_dir().join(format!("lc-ui-pin-object-{}", std::process::id())); + h.app.session.segmenter.dir = Some(dir.clone()); + h.app.session.segmenter.mirrors_file = Some(dir.join("none.txt")); + let object = |at: Point| MaskShape::Object { hint: vec![at], exclude: vec![], seg: None, detail: vec![], edge: 0.0 }; + let radial = json!({"kind": "radial", "center": [0.5, 0.5], "rx": 0.1, "ry": 0.1}); + exec(&mut h, "mask.add", radial.clone()); + exec(&mut h, "mask.update", json!({"id": 1, "shape": object(Point::new(0.6, 0.5))})); + exec(&mut h, "mask.add", json!({"kind": "radial", "center": [0.2, 0.8], "rx": 0.05, "ry": 0.05})); + exec(&mut h, "mask.add", radial); + exec(&mut h, "mask.update", json!({"id": 3, "shape": object(Point::new(0.3, 0.3))})); + assert_eq!(h.app.session.active_mask, Some(3)); + exec(&mut h, "view.toolOverlay", json!({"mode": "always"})); + h.app.ui.tool = "object".into(); + h.settle(SETTLE); + let toast = h.app.ui.toast.as_ref().map(|t| t.0.clone()); + let before = develop(&h).masks; + // the Object mask's pin: it is selected, its clicks and the selected mask's stay as they were + pointer(&mut h, json!([{"kind": "down", "x": 0.6, "y": 0.5}, {"kind": "up", "x": 0.6, "y": 0.5}])); + h.settle(SETTLE); + assert_eq!(h.app.session.active_mask, Some(1), "the pinned mask is selected"); + assert_eq!(develop(&h).masks, before, "no Object click was added to any mask"); + assert_eq!(h.app.ui.dialog, None, "no Object click was tried"); + assert_eq!(h.app.ui.toast.as_ref().map(|t| t.0.clone()).filter(|t| Some(t) != toast.as_ref()), None, "no error"); + assert_eq!(h.app.ui.tool, "object", "the tool stays on to refine the Object mask"); + // the Radial mask's pin: selected, and the Object tool goes off (nothing for it to refine) + pointer(&mut h, json!([{"kind": "down", "x": 0.2, "y": 0.8}, {"kind": "up", "x": 0.2, "y": 0.8}])); + h.settle(SETTLE); + assert_eq!(h.app.session.active_mask, Some(2)); + assert_eq!(develop(&h).masks, before); + assert_eq!(h.app.ui.tool, "", "the Object tool is off"); + assert_eq!(h.app.ui.dialog, None); +} + /// #2210: Ctrl+Shift+O (the all-masks overlay) draws the white outline of every visible /// Radial/Linear mask, not just the selected one — but only the selected mask registers /// `maskHandle:` elements, so nothing else becomes draggable. diff --git a/docs/whats-new.md b/docs/whats-new.md index 9583209df..525115bf5 100644 --- a/docs/whats-new.md +++ b/docs/whats-new.md @@ -6,7 +6,8 @@ - With Show Pins on, an Object mask now has a pin where you first clicked the object, and a Describe mask has one in the middle of what its description found. Before, these masks had no pin on the photo, so they couldn't be picked there. Clicking a pin selects its mask; dragging it changes nothing, as these masks follow what they selected. - Subject, Sky, Background and the range masks still have no pin. + With the Object tool on, a click on another mask's pin selects that mask too, instead of adding a click to the + selected Object mask. Subject, Sky, Background and the range masks still have no pin. ## 2026-10-24