diff --git a/crates/ui-egui/src/panels/detail/masks.rs b/crates/ui-egui/src/panels/detail/masks.rs index c67823db0..1c649c7c5 100644 --- a/crates/ui-egui/src/panels/detail/masks.rs +++ b/crates/ui-egui/src/panels/detail/masks.rs @@ -467,9 +467,16 @@ fn other_pin_at(grips: &[(u32, usize, u8, Pos2)], active: Option, q: Pos2) .map(|g| g.0) } +/// Whether `q` is on one of the `active` mask's own pins (within the same 10 px). +fn own_pin_at(grips: &[(u32, usize, u8, Pos2)], active: Option, q: Pos2) -> bool { + active.is_some_and(|a| grips.iter().any(|g| g.0 == a && g.2 == 0 && g.3.distance(q) < 10.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. +/// the tool then stays on only if the newly selected mask has an Object selection to refine. A +/// click on the selected mask's own pin does nothing: its first click is already there, and a +/// near-copy of it would use up one of the mask's limited Object clicks. fn object_tool( app: &mut LightcraftApp, ui: &mut egui::Ui, @@ -493,6 +500,9 @@ fn object_tool( } return; } + if own_pin_at(grips, app.session.active_mask, q) { + 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 4e7358d3f..027d89a33 100644 --- a/crates/ui-egui/src/tests/masking.rs +++ b/crates/ui-egui/src/tests/masking.rs @@ -449,6 +449,36 @@ fn the_object_tool_selects_the_mask_whose_pin_is_clicked() { assert_eq!(h.app.ui.dialog, None); } +/// Issue #3049: with the Object tool on, a click on the selected Object mask's own pin adds no +/// near-copy of its first click (which would use up one of its limited Object clicks): its +/// clicks stay as they were, it stays selected and no error is shown. +#[test] +fn the_object_tool_adds_no_click_on_the_selected_masks_own_pin() { + 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-own-pin-object-{}", std::process::id())); + h.app.session.segmenter.dir = Some(dir.clone()); + h.app.session.segmenter.mirrors_file = Some(dir.join("none.txt")); + exec(&mut h, "mask.add", json!({"kind": "radial", "center": [0.5, 0.5], "rx": 0.1, "ry": 0.1})); + let object = MaskShape::Object { hint: vec![Point::new(0.6, 0.5)], exclude: vec![], seg: None, detail: vec![], edge: 0.0 }; + exec(&mut h, "mask.update", json!({"id": 1, "shape": object})); + assert_eq!(h.app.session.active_mask, Some(1)); + 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; + // a little off the pin's centre, as a hand clicks it + pointer(&mut h, json!([{"kind": "down", "x": 0.601, "y": 0.501}, {"kind": "up", "x": 0.601, "y": 0.501}])); + h.settle(SETTLE); + assert_eq!(develop(&h).masks, before, "no Object click was added"); + assert_eq!(h.app.session.active_mask, Some(1), "the mask stays selected"); + 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"); +} + /// #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 525115bf5..7fff88ea1 100644 --- a/docs/whats-new.md +++ b/docs/whats-new.md @@ -7,7 +7,8 @@ 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. 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. + selected Object mask, and a click on the selected mask's own pin adds nothing. Subject, Sky, Background and the + range masks still have no pin. ## 2026-10-24