From cdffdcbbefb7a3fd4cf5c33be3b0601e5d0f1325 Mon Sep 17 00:00:00 2001 From: Colin Faulkingham Date: Fri, 30 Oct 2026 19:03:10 +0000 Subject: [PATCH] Selection mode: the Modelling SELECT panel shows and clears component picking (#9382) (#9485) * claim: #9382 * Show component picking in the Modelling Select panel (#9382) --- .../pending/select-components-by-face.json | 10 +++++++ crates/ui-egui/src/lib.rs | 12 ++++++++ crates/ui-egui/src/render_ui/mod.rs | 12 +------- crates/ui-egui/src/render_workspace_tests.rs | 29 +++++++++++++++++++ crates/ui-egui/src/toolbar.rs | 10 +++++-- 5 files changed, 60 insertions(+), 13 deletions(-) create mode 100644 assets/locales/pending/select-components-by-face.json diff --git a/assets/locales/pending/select-components-by-face.json b/assets/locales/pending/select-components-by-face.json new file mode 100644 index 0000000000..62b3a9aebe --- /dev/null +++ b/assets/locales/pending/select-components-by-face.json @@ -0,0 +1,10 @@ +{ + "en": { "ui.select_components_by_face": "Select components (a face picks its component)" }, + "ar": { "ui.select_components_by_face": "تحديد المكوّنات (تحديد وجه يحدّد المكوّن الذي ينتمي إليه)" }, + "de": { "ui.select_components_by_face": "Komponenten auswählen (eine Fläche wählt ihre Komponente)" }, + "he": { "ui.select_components_by_face": "בחירת רכיבים (בחירת פאה בוחרת את הרכיב שלה)" }, + "ja": { "ui.select_components_by_face": "コンポーネントを選択(面をクリックするとそのコンポーネントが選択されます)" }, + "ko": { "ui.select_components_by_face": "구성요소 선택(면을 클릭하면 해당 구성요소 선택)" }, + "vi": { "ui.select_components_by_face": "Chọn thành phần (chọn một mặt sẽ chọn thành phần chứa mặt đó)" }, + "zh": { "ui.select_components_by_face": "选择组件(单击面即选择其所属组件)" } +} diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index e58e1c0283..51654b5208 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -368,6 +368,18 @@ pub struct UiState { pub tool_cursor: bool, } +impl UiState { + fn pick_mode(&self) -> &'static str { + if self.pick_components { + "component" + } else if self.pick_bodies { + "body" + } else { + "face" + } + } +} + impl Default for UiState { fn default() -> Self { UiState { diff --git a/crates/ui-egui/src/render_ui/mod.rs b/crates/ui-egui/src/render_ui/mod.rs index f7f5766e7c..4b6b9d4c1b 100644 --- a/crates/ui-egui/src/render_ui/mod.rs +++ b/crates/ui-egui/src/render_ui/mod.rs @@ -85,7 +85,7 @@ fn stage_tools(app: &mut SolveApp, ui: &mut egui::Ui) { Stage::Look => { ui_kit::StageBar::caption(ui, app.tr("render.pick_label")); let modes = ["body", "face", "component"]; - let mut selected = modes.iter().position(|mode| *mode == current_pick_mode(app)).unwrap_or(0); + let mut selected = modes.iter().position(|mode| *mode == app.ui.pick_mode()).unwrap_or(0); let pick = ui.add_enabled_ui(!crate::render_workspace::result_active(app), |ui| { if ui_kit::segmented( ui, @@ -225,16 +225,6 @@ pub(crate) fn dock_chip(app: &mut SolveApp, ui: &mut egui::Ui, id: &'static str, crate::shell::register_panel(app, id, Some(chip.response.rect), None); } -fn current_pick_mode(app: &SolveApp) -> &'static str { - if app.ui.pick_components { - "component" - } else if app.ui.pick_bodies { - "body" - } else { - "face" - } -} - fn shots_pill(app: &mut SolveApp, ui: &mut egui::Ui, free: Rect) { crate::shell::register_panel(app, "render:shots:show", None, None); if app.preferences.render_shots_strip { diff --git a/crates/ui-egui/src/render_workspace_tests.rs b/crates/ui-egui/src/render_workspace_tests.rs index 350d765833..8108bab2b4 100644 --- a/crates/ui-egui/src/render_workspace_tests.rs +++ b/crates/ui-egui/src/render_workspace_tests.rs @@ -4431,6 +4431,35 @@ fn render_workspace_pick_stage_tools_filter_sample_clicks_with_shots_shown_and_h assert!(h.app.tool.is_none() && h.app.dialog.is_none()); } +#[test] +fn modelling_select_panel_shows_and_clears_renders_component_pick() { + use egui::accesskit::{Role, Toggled}; + const BODY: &str = "Select bodies (a face picks its body)"; + const COMPONENT: &str = "Select components (a face picks its component)"; + let (mut h, _) = render_pick_sample(); + h.step(&json!({"click":{"handle":"render:pick:component"}})).unwrap(); + enter(&mut h.app, false).unwrap(); + h.settle(); + h.step(&json!({"click":{"handle":"tab:VIEW"}})).unwrap(); + h.step(&json!({"click":{"handle":"panel:VIEW:SELECT"}})).unwrap(); + + let check = |h: &Harness, body: bool, component: bool| { + assert_eq!((h.app.ui.pick_bodies, h.app.ui.pick_components), (body, component)); + let tree = h.output.accesskit_update.as_ref().unwrap(); + for (label, on) in [(BODY, body), (COMPONENT, component)] { + let node = tree.nodes.iter().find(|(_, n)| n.role() == Role::CheckBox && n.label() == Some(label)); + assert_eq!(node.and_then(|(_, n)| n.toggled()), Some(if on { Toggled::True } else { Toggled::False }), "{label}"); + } + }; + check(&h, false, true); + h.step(&json!({"widget":"Faces"})).unwrap(); + for (label, body, component) in [(COMPONENT, false, false), (BODY, true, false), (COMPONENT, false, true), (BODY, true, false)] { + h.step(&json!({"widget":label})).unwrap(); + check(&h, body, component); + assert!(h.app.ui.pick_off.iter().any(|kind| kind == "faces"), "changing pick mode must preserve the panel's Faces filter"); + } +} + #[test] fn render_workspace_click_modifiers_clear_and_browser_share_selection() { use solidcraft_engine::Sel; diff --git a/crates/ui-egui/src/toolbar.rs b/crates/ui-egui/src/toolbar.rs index 5caad33f38..4ad86e43c7 100644 --- a/crates/ui-egui/src/toolbar.rs +++ b/crates/ui-egui/src/toolbar.rs @@ -2881,8 +2881,14 @@ fn drag_ghost(app: &SolveApp, ctx: &egui::Context, id: &str, at: egui::Pos2, rem pub(crate) fn selection_filter(app: &mut SolveApp, ui: &mut egui::Ui) { let t = Tokens::get(); ui.label(egui::RichText::new(crate::i18n::tr_label("Selection filter")).color(t.text_dim).size(t.font_small)); - if crate::ui_kit::checkbox(ui, crate::i18n::tr_label("Select bodies (a face picks its body)"), &mut app.ui.pick_bodies).changed() { - app.ui.pick_components = false; + for (mode, label) in [("body", app.tr_label("Select bodies (a face picks its body)")), ("component", app.tr("ui.select_components_by_face"))] { + let mut on = app.ui.pick_mode() == mode; + if ui_kit::checkbox(ui, label, &mut on).changed() { + // The Faces checkbox below is independent. The standalone command enables faces. + let filters = std::mem::take(&mut app.ui.pick_off); + let _ = app.run("select.filter", json!({"mode": if on { mode } else { "face" }})); + app.ui.pick_off = filters; + } } for (k, label) in [ ("faces", "Faces"),