Keep the selection when smart selection finds nothing (#2235)

* Keep the selection when smart selection finds nothing

Select Subject and Object Selection run heuristics that often miss on busy
photos. In Replace mode an empty result cleared the selection, so the Select
Subject button looked dead and even destroyed existing work. apply() now
preserves the selection and reports changed:false in every mode.

The UI also answers visibly instead of staying silent: empty smart-selection
results set an info status suggesting what to try next, and a click with
Object Selection falls back to a Quick Selection dab at the brush size
instead of doing nothing.

* Translate the smart-selection status messages; drop needless borrows (clippy)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Vikram Bhaskaran 2026-10-10 16:42:28 +11:00 committed by GitHub
parent 5e481cde02
commit 39c97c9be0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 159 additions and 11 deletions

View File

@ -60,8 +60,12 @@ pub(crate) fn with_doc_sampler<R>(doc: &Document, layer: Option<LayerId>, all_la
}
/// Stores `region` combined with the current selection by `m` as one history step.
/// Finding nothing never destroys work: the selection (and history) is left alone and the
/// caller gets `changed: false`, whatever the mode. (Select Subject and Object Selection run
/// heuristics that often find nothing on busy photos; clearing the selection then looks like
/// a dead button.)
fn apply(s: &mut Session, label: &str, region: Option<Region>, m: SelectionMode) -> Result<Value> {
if region.is_none() && matches!(m, SelectionMode::Add | SelectionMode::Subtract) {
if region.is_none() {
// Nothing found: leave the selection (and history) alone.
let selected = s.active().ok_or(EngineError::NoDocument)?.doc.selection.is_some();
return Ok(json!({ "selected": selected, "changed": false }));
@ -388,6 +392,27 @@ mod tests {
assert!(cov(&s, 10, 10) >= 0.5 && cov(&s, 100, 50) < 0.5);
}
#[test]
fn empty_result_keeps_the_selection_in_every_mode() {
// Finding nothing must never wipe the selection, whatever the mode: Select Subject
// and Object Selection run heuristics that often miss on busy photos, and a missed
// Quick Selection dab must not destroy the work either.
for m in ["add", "subtract", "replace"] {
let mut s = two_regions(8);
s.execute("select.all", json!({})).unwrap();
let r = s.execute("select.quick", json!({"points": [[-5000, -5000]], "size": 8, "mode": m})).unwrap();
assert_eq!(r["changed"], false, "mode {m}");
assert_eq!(r["selected"], true, "mode {m}");
assert!(cov(&s, 10, 50) >= 0.99, "mode {m}: Select All survived an empty result");
}
// Select Subject on a flat field finds nothing and keeps the selection too.
let mut s = session_with(140, 100, 8, |_, _| [0.5, 0.5, 0.5]);
s.execute("select.all", json!({})).unwrap();
let r = s.execute("select.subject", json!({})).unwrap();
assert_eq!(r["changed"], false);
assert!(cov(&s, 10, 10) >= 0.99);
}
#[test]
fn object_selection_disc() {
for depth in [8, 16, 32] {

View File

@ -2356,3 +2356,7 @@ fontWeight Italic Kurzíva
XOR XOR
Color Burn (Paint.NET) Ztmavit barvy (Paint.NET)
Color Dodge (Paint.NET) Zesvětlit barvy (Paint.NET)
No subject found — try Object Selection or the Lasso Nebyl nalezen žádný objekt — zkuste Výběr objektu nebo Laso
Quick Selection found nothing there Rychlý výběr zde nic nenašel
Object Selection found nothing there — try a tighter rectangle Výběr objektu zde nic nenašel — zkuste těsnější obdélník
No subject found — try a rectangle around the object Nebyl nalezen žádný objekt — zkuste obdélník kolem objektu

Can't render this file because it contains an unexpected character in line 6 and column 47.

View File

@ -2395,3 +2395,7 @@ fontWeight Italic Kursiv
XOR XOR
Color Burn (Paint.NET) Farbig nachbelichten (Paint.NET)
Color Dodge (Paint.NET) Farbig abwedeln (Paint.NET)
No subject found — try Object Selection or the Lasso Kein Motiv gefunden — versuchen Sie das Objektauswahlwerkzeug oder das Lasso
Quick Selection found nothing there Die Schnellauswahl hat dort nichts gefunden
Object Selection found nothing there — try a tighter rectangle Die Objektauswahl hat dort nichts gefunden — versuchen Sie ein engeres Rechteck
No subject found — try a rectangle around the object Kein Motiv gefunden — versuchen Sie ein Rechteck um das Objekt

Can't render this file because it contains an unexpected character in line 6 and column 47.

View File

@ -3467,3 +3467,7 @@ fontWeight Thin Πολύ λεπτά
XOR XOR
Color Burn (Paint.NET) Κάψιμο χρώματος (Paint.NET)
Color Dodge (Paint.NET) Φωτισμός χρώματος (Paint.NET)
No subject found — try Object Selection or the Lasso Δεν βρέθηκε θέμα — δοκιμάστε την Επιλογή αντικειμένου ή το Λάσο
Quick Selection found nothing there Η Γρήγορη επιλογή δεν βρήκε τίποτα εκεί
Object Selection found nothing there — try a tighter rectangle Η Επιλογή αντικειμένου δεν βρήκε τίποτα εκεί — δοκιμάστε ένα πιο στενό ορθογώνιο
No subject found — try a rectangle around the object Δεν βρέθηκε θέμα — δοκιμάστε ένα ορθογώνιο γύρω από το αντικείμενο

Can't render this file because it has a wrong number of fields in line 5.

View File

@ -2424,3 +2424,7 @@ fontWeight Italic Cursiva
XOR XOR
Color Burn (Paint.NET) Sobreexponer color (Paint.NET)
Color Dodge (Paint.NET) Aclarar color (Paint.NET)
No subject found — try Object Selection or the Lasso No se encontró ningún sujeto — pruebe Selección de objeto o el Lazo
Quick Selection found nothing there Selección rápida no encontró nada ahí
Object Selection found nothing there — try a tighter rectangle Selección de objeto no encontró nada ahí — pruebe un rectángulo más ajustado
No subject found — try a rectangle around the object No se encontró ningún sujeto — pruebe un rectángulo alrededor del objeto

Can't render this file because it contains an unexpected character in line 4 and column 47.

View File

@ -2777,3 +2777,7 @@ fontWeight Italic Italique
XOR XOR
Color Burn (Paint.NET) Densité couleur + (Paint.NET)
Color Dodge (Paint.NET) Densité couleur − (Paint.NET)
No subject found — try Object Selection or the Lasso Aucun sujet trouvé — essayez la Sélection d'objet ou le Lasso
Quick Selection found nothing there La Sélection rapide n'a rien trouvé à cet endroit
Object Selection found nothing there — try a tighter rectangle La Sélection d'objet n'a rien trouvé à cet endroit — essayez un rectangle plus serré
No subject found — try a rectangle around the object Aucun sujet trouvé — essayez un rectangle autour de l'objet

Can't render this file because it contains an unexpected character in line 6 and column 47.

View File

@ -2764,3 +2764,7 @@ fontWeight Italic Miring
XOR XOR
Color Burn (Paint.NET) Color Burn (Paint.NET)
Color Dodge (Paint.NET) Color Dodge (Paint.NET)
No subject found — try Object Selection or the Lasso Subjek tidak ditemukan — coba Seleksi Objek atau Laso
Quick Selection found nothing there Seleksi Cepat tidak menemukan apa pun di sana
Object Selection found nothing there — try a tighter rectangle Seleksi Objek tidak menemukan apa pun di sana — coba persegi panjang yang lebih rapat
No subject found — try a rectangle around the object Subjek tidak ditemukan — coba persegi panjang di sekitar objek

Can't render this file because it contains an unexpected character in line 4 and column 47.

View File

@ -2426,3 +2426,7 @@ fontWeight Italic Corsivo
XOR XOR
Color Burn (Paint.NET) Colore brucia (Paint.NET)
Color Dodge (Paint.NET) Colore scherma (Paint.NET)
No subject found — try Object Selection or the Lasso Nessun soggetto trovato — prova Selezione oggetto o il Lazo
Quick Selection found nothing there Selezione rapida non ha trovato nulla lì
Object Selection found nothing there — try a tighter rectangle Selezione oggetto non ha trovato nulla lì — prova un rettangolo più stretto
No subject found — try a rectangle around the object Nessun soggetto trovato — prova un rettangolo attorno all'oggetto

Can't render this file because it has a wrong number of fields in line 14.

View File

@ -3380,3 +3380,7 @@ fontWeight Italic 斜体
XOR XOR
Color Burn (Paint.NET) 焼き込みカラー (Paint.NET)
Color Dodge (Paint.NET) 覆い焼きカラー (Paint.NET)
No subject found — try Object Selection or the Lasso 被写体が見つかりません — オブジェクト選択ツールまたはなげなわツールを試してください
Quick Selection found nothing there クイック選択ではそこに何も見つかりませんでした
Object Selection found nothing there — try a tighter rectangle オブジェクト選択ではそこに何も見つかりませんでした — より狭い長方形で試してください
No subject found — try a rectangle around the object 被写体が見つかりません — オブジェクトを囲む長方形を試してください

Can't render this file because it contains an unexpected character in line 6 and column 47.

View File

@ -2935,3 +2935,7 @@ fontWeight Italic 기울임
XOR XOR
Color Burn (Paint.NET) 색상 번 (Paint.NET)
Color Dodge (Paint.NET) 색상 닷지 (Paint.NET)
No subject found — try Object Selection or the Lasso 피사체를 찾을 수 없습니다 — 개체 선택 또는 올가미를 사용해 보세요
Quick Selection found nothing there 빠른 선택으로 해당 위치에서 아무것도 찾지 못했습니다
Object Selection found nothing there — try a tighter rectangle 개체 선택으로 해당 위치에서 아무것도 찾지 못했습니다 — 더 좁은 사각형을 사용해 보세요
No subject found — try a rectangle around the object 피사체를 찾을 수 없습니다 — 개체 주위에 사각형을 그려 보세요

Can't render this file because it has a wrong number of fields in line 10.

View File

@ -3539,3 +3539,7 @@ cameraRaw Whites Witten
Color Dodge (Paint.NET) Kleur tegenhouden (Paint.NET)
Pro Medium Pro Medium
Studio Light Studio Light
No subject found — try Object Selection or the Lasso Geen onderwerp gevonden — probeer Objectselectie of de Lasso
Quick Selection found nothing there Snelle selectie heeft daar niets gevonden
Object Selection found nothing there — try a tighter rectangle Objectselectie heeft daar niets gevonden — probeer een krappere rechthoek
No subject found — try a rectangle around the object Geen onderwerp gevonden — probeer een rechthoek rond het object

Can't render this file because it contains an unexpected character in line 8 and column 34.

View File

@ -3379,3 +3379,7 @@ cameraRaw Roughness Chropowatość
XOR XOR
Color Burn (Paint.NET) Wypalanie koloru (Paint.NET)
Color Dodge (Paint.NET) Rozjaśnianie koloru (Paint.NET)
No subject found — try Object Selection or the Lasso Nie znaleziono obiektu — spróbuj Zaznaczania obiektu lub Lassa
Quick Selection found nothing there Szybkie zaznaczanie nic tam nie znalazło
Object Selection found nothing there — try a tighter rectangle Zaznaczanie obiektu nic tam nie znalazło — spróbuj ciaśniejszego prostokąta
No subject found — try a rectangle around the object Nie znaleziono obiektu — spróbuj prostokąta wokół obiektu

Can't render this file because it contains an unexpected character in line 6 and column 47.

View File

@ -2785,3 +2785,7 @@ fontWeight Italic Itálico
XOR XOR
Color Burn (Paint.NET) Superexposição de cores (Paint.NET)
Color Dodge (Paint.NET) Subexposição de cores (Paint.NET)
No subject found — try Object Selection or the Lasso Nenhum assunto encontrado — tente Seleção de objeto ou o Laço
Quick Selection found nothing there A Seleção rápida não encontrou nada ali
Object Selection found nothing there — try a tighter rectangle A Seleção de objeto não encontrou nada ali — tente um retângulo mais justo
No subject found — try a rectangle around the object Nenhum assunto encontrado — tente um retângulo ao redor do objeto

Can't render this file because it contains an unexpected character in line 6 and column 47.

View File

@ -2631,3 +2631,7 @@ fontWeight Italic Курсив
XOR XOR
Color Burn (Paint.NET) Затемнение основы (Paint.NET)
Color Dodge (Paint.NET) Осветление основы (Paint.NET)
No subject found — try Object Selection or the Lasso Объект не найден — попробуйте «Выделение объектов» или «Лассо»
Quick Selection found nothing there Быстрое выделение ничего здесь не нашло
Object Selection found nothing there — try a tighter rectangle Выделение объектов ничего здесь не нашло — попробуйте более узкий прямоугольник
No subject found — try a rectangle around the object Объект не найден — попробуйте прямоугольник вокруг объекта

Can't render this file because it contains an unexpected character in line 6 and column 47.

View File

@ -3466,3 +3466,7 @@ cameraRaw Masking Маскування
Color Dodge (Paint.NET) Освітлення основи (Paint.NET)
Pro Medium Pro Medium
Studio Light Studio Light
No subject found — try Object Selection or the Lasso Об'єкт не знайдено — спробуйте «Виділення об'єктів» або «Ласо»
Quick Selection found nothing there Швидке виділення нічого тут не знайшло
Object Selection found nothing there — try a tighter rectangle Виділення об'єктів нічого тут не знайшло — спробуйте вужчий прямокутник
No subject found — try a rectangle around the object Об'єкт не знайдено — спробуйте прямокутник навколо об'єкта

Can't render this file because it has a wrong number of fields in line 12.

View File

@ -2364,3 +2364,7 @@ fontWeight Italic 斜体
XOR XOR
Color Burn (Paint.NET) 颜色加深 (Paint.NET)
Color Dodge (Paint.NET) 颜色减淡 (Paint.NET)
No subject found — try Object Selection or the Lasso 未找到主体 — 请尝试对象选择工具或套索工具
Quick Selection found nothing there 快速选择在该处未找到任何内容
Object Selection found nothing there — try a tighter rectangle 对象选择在该处未找到任何内容 — 请尝试更紧凑的矩形
No subject found — try a rectangle around the object 未找到主体 — 请尝试在对象周围绘制矩形

Can't render this file because it has a wrong number of fields in line 14.

View File

@ -2333,3 +2333,7 @@ fontWeight Italic 斜體
XOR XOR
Color Burn (Paint.NET) 加深顏色 (Paint.NET)
Color Dodge (Paint.NET) 加亮顏色 (Paint.NET)
No subject found — try Object Selection or the Lasso 找不到主體 — 請嘗試物件選取工具或套索工具
Quick Selection found nothing there 快速選取在該處找不到任何內容
Object Selection found nothing there — try a tighter rectangle 物件選取在該處找不到任何內容 — 請嘗試更緊密的矩形
No subject found — try a rectangle around the object 找不到主體 — 請嘗試在物件周圍繪製矩形

Can't render this file because it contains an unexpected character in line 6 and column 47.

View File

@ -1009,7 +1009,8 @@ pub fn options_bar(app: &mut PhotocraftApp, ui: &mut egui::Ui) {
widgets::checkbox(ui, &mut app.ui.tool_options.sample_all_layers, tl!("Sample All Layers"));
widgets::vline(ui, 22.0);
if widgets::secondary_button(ui, tl!("Select Subject"), 0.0).clicked() {
let _ = app.run("select.subject", json!({}));
let r = app.run("select.subject", json!({}));
crate::retouch_ui::report_smart_result(app, r, tl!("No subject found — try Object Selection or the Lasso"));
}
if widgets::secondary_button(ui, tl!("Select and Mask…"), 0.0).clicked() {
let _ = crate::menus::invoke(app, ui.ctx(), "select.selectAndMask", json!({}));

View File

@ -103,8 +103,9 @@ pub fn finish_stroke(app: &mut PhotocraftApp, tool: Tool, points: &[[f64; 3]], m
let size = app.session.tools.brush.size;
let mode = if mods.alt { "subtract" } else { "add" };
let xy: Vec<[f64; 2]> = points.iter().map(|q| [q[0], q[1]]).collect();
let _ = app
.run("select.quick", json!({"points": xy, "size": size, "mode": mode, "enhanceEdge": o.enhance_edge, "sampleAllLayers": o.sample_all_layers}));
let (sample_all, enhance) = (o.sample_all_layers, o.enhance_edge);
let r = app.run("select.quick", json!({"points": xy, "size": size, "mode": mode, "enhanceEdge": enhance, "sampleAllLayers": sample_all}));
report_smart_result(app, r, tl!("Quick Selection found nothing there"));
return true;
}
_ => return false,
@ -203,11 +204,19 @@ pub fn finish_content_aware_move(app: &mut PhotocraftApp, start: [f64; 2], end:
}
}
/// Object Selection: the dragged rectangle.
/// Object Selection: the dragged rectangle. A click (or a sub-2 px nudge) cannot be a
/// rectangle: like Photoshop's click-to-select, it falls back to a Quick Selection dab at
/// the brush size instead of looking dead (⌥ subtracts, otherwise it adds). Larger drags
/// run the rectangle GrabCut (⌥ subtracts, ⇧ adds, otherwise replaces).
pub fn finish_object_selection(app: &mut PhotocraftApp, start: [f64; 2], end: [f64; 2], mods: egui::Modifiers) {
let (x, y) = (start[0].min(end[0]), start[1].min(end[1]));
let (w, h) = ((end[0] - start[0]).abs(), (end[1] - start[1]).abs());
let sample_all = app.ui.tool_options.sample_all_layers;
if w < 2.0 || h < 2.0 {
let size = app.session.tools.brush.size;
let mode = if mods.alt { "subtract" } else { "add" };
let r = app.run("select.quick", json!({"points": [[end[0], end[1]]], "size": size, "mode": mode, "sampleAllLayers": sample_all}));
report_smart_result(app, r, tl!("Quick Selection found nothing there"));
return;
}
let mode = if mods.alt {
@ -217,10 +226,19 @@ pub fn finish_object_selection(app: &mut PhotocraftApp, start: [f64; 2], end: [f
} else {
"replace"
};
let _ = app.run(
"select.object",
json!({"rect": [x.round(), y.round(), w.round(), h.round()], "mode": mode, "sampleAllLayers": app.ui.tool_options.sample_all_layers}),
);
let r = app.run("select.object", json!({"rect": [x.round(), y.round(), w.round(), h.round()], "mode": mode, "sampleAllLayers": sample_all}));
report_smart_result(app, r, tl!("Object Selection found nothing there — try a tighter rectangle"));
}
/// Reports a smart-selection command result in the status bar. Engine errors are already
/// reported by [`PhotocraftApp::run`], but an empty result (`changed: false`) is a silent
/// success that looks like a dead button (Select Subject finding nothing on a busy photo,
/// Object Selection missing its rectangle). `empty_msg` says what to try next.
pub fn report_smart_result(app: &mut PhotocraftApp, res: Result<Value, String>, empty_msg: &str) {
if matches!(res, Ok(ref v) if v.get("changed").and_then(Value::as_bool) == Some(false)) {
app.ui.status = empty_msg.into();
app.ui.status_error = false;
}
}
/// ⌥-click with Clone Stamp / Healing Brush sets the source.
@ -356,7 +374,8 @@ pub fn options_bar(app: &mut PhotocraftApp, ui: &mut egui::Ui, tool: Tool) -> bo
opt(ui, &crate::i18n::fmt(tl!("{key} to subtract"), &[("key", &crate::shortcuts::pretty("Alt"))]));
crate::widgets::vline(ui, 22.0);
if crate::widgets::secondary_button(ui, tl!("Select Subject"), 0.0).clicked() {
let _ = app.run("select.subject", json!({}));
let r = app.run("select.subject", json!({}));
report_smart_result(app, r, tl!("No subject found — try Object Selection or the Lasso"));
}
}
Tool::ObjectSelection => {
@ -364,7 +383,8 @@ pub fn options_bar(app: &mut PhotocraftApp, ui: &mut egui::Ui, tool: Tool) -> bo
opt(ui, tl!("Drag a rectangle around the object"));
crate::widgets::vline(ui, 22.0);
if crate::widgets::secondary_button(ui, tl!("Select Subject"), 0.0).clicked() {
let _ = app.run("select.subject", json!({}));
let r = app.run("select.subject", json!({}));
report_smart_result(app, r, tl!("No subject found — try a rectangle around the object"));
}
}
Tool::RedEye => {
@ -511,6 +531,44 @@ mod tests {
}
}
#[test]
fn object_selection_click_falls_back_to_quick_selection() {
// A click cannot be a rectangle: it selects at the cursor instead of looking dead.
let mut app = app();
app.ui.tool = Tool::ObjectSelection;
let m = egui::Modifiers::NONE;
tool_event(&mut app, ToolEvent::Down { x: 50.0, y: 30.0, pressure: 1.0 }, m);
tool_event(&mut app, ToolEvent::Up { x: 50.0, y: 30.0 }, m);
assert!(!app.ui.status_error, "{}", app.ui.status);
assert_eq!(app.session.journal.last().map(|(id, _)| id.as_str()), Some("select.quick"));
}
#[test]
fn object_selection_rect_runs_object_selection_and_reports() {
let mut app = app();
app.run("paint.pencil", json!({"points": [[20, 10], [20, 50]], "size": 8, "color": "#ff0000"})).unwrap();
app.ui.tool = Tool::ObjectSelection;
let m = egui::Modifiers::NONE;
tool_event(&mut app, ToolEvent::Down { x: 5.0, y: 5.0, pressure: 1.0 }, m);
tool_event(&mut app, ToolEvent::Move { x: 60.0, y: 55.0, pressure: 1.0 }, m);
tool_event(&mut app, ToolEvent::Up { x: 60.0, y: 55.0 }, m);
assert!(!app.ui.status_error, "{}", app.ui.status);
assert_eq!(app.session.journal.last().map(|(id, _)| id.as_str()), Some("select.object"));
}
#[test]
fn empty_smart_result_is_reported_without_error() {
// An empty smart selection (Select Subject missing on a busy photo) is an info, not
// an error and not silence: the button must visibly answer.
let mut app = app();
report_smart_result(&mut app, Ok(json!({"selected": false, "changed": false})), "nothing there");
assert_eq!(app.ui.status.as_str(), "nothing there");
assert!(!app.ui.status_error);
app.ui.status.clear();
report_smart_result(&mut app, Ok(json!({"selected": true, "changed": true})), "nothing there");
assert!(app.ui.status.is_empty());
}
#[test]
fn mixer_brush_is_selectable_and_paints_only_inside_the_selection_via_control() {
use crate::control::{ControlRequest, Outcome, handle};