From 24627d4f0a1f0c025e5176139e04a83a83d3de09 Mon Sep 17 00:00:00 2001 From: bherbruck Date: Thu, 8 Oct 2026 00:09:01 +0000 Subject: [PATCH] Selection filter in the SELECT panel: select bodies (a face picks and highlights its body), faces, edges, vertices, sketch geometry Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL --- crates/ui-egui/src/lib.rs | 6 ++++++ crates/ui-egui/src/toolbar.rs | 22 ++++++++++++++++++++-- crates/ui-egui/src/viewport.rs | 26 +++++++++++++++++++++++--- 3 files changed, 49 insertions(+), 5 deletions(-) diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 51d041f6..f127d9f7 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -55,6 +55,10 @@ pub struct UiState { pub palette_open: bool, /// Dark theme (the default); a preference kept between runs. pub dark: bool, + /// Selection filter: kinds a click doesn't pick (`vertices`, `edges`, `faces`, `sketch`). + pub pick_off: Vec, + /// A click on a face picks its whole body. + pub pick_bodies: bool, } impl Default for UiState { @@ -71,6 +75,8 @@ impl Default for UiState { show_sketches: true, palette_open: false, dark: true, + pick_off: Vec::new(), + pick_bodies: false, } } } diff --git a/crates/ui-egui/src/toolbar.rs b/crates/ui-egui/src/toolbar.rs index 9cc62346..22e4acfd 100644 --- a/crates/ui-egui/src/toolbar.rs +++ b/crates/ui-egui/src/toolbar.rs @@ -248,7 +248,7 @@ pub fn toolbar(app: &mut SolveApp, ui: &mut egui::Ui) { let cmds: Vec<_> = specs.iter().filter(|c| c.tab == tab && c.panel == *panel).collect(); let n = promoted(&tab, panel).min(cmds.len()).max(if cmds.is_empty() { 1 } else { 0 }); let width = (n as f32 * 40.0).max(64.0) + 8.0; - let enabled_panel = !cmds.is_empty(); + let enabled_panel = !cmds.is_empty() || *panel == "SELECT"; for (i, c) in cmds.iter().take(n).enumerate() { let br = Rect::from_min_size(pos2(px + 4.0 + i as f32 * 40.0, top + 2.0), vec2(38.0, 38.0)); let info = c.info(&app.session); @@ -305,7 +305,9 @@ pub fn toolbar(app: &mut SolveApp, ui: &mut egui::Ui) { let area = egui::Area::new(popup_id).fixed_pos(lr.left_bottom()).order(egui::Order::Foreground).show(ui.ctx(), |ui| { egui::Frame::popup(ui.style()).show(ui, |ui| { ui.set_min_width(200.0); - if cmds.is_empty() { + if *panel == "SELECT" { + selection_filter(app, ui); + } else if cmds.is_empty() { ui.label(egui::RichText::new("Not available yet").color(t.text_dim)); } for c in &cmds { @@ -335,6 +337,22 @@ pub fn toolbar(app: &mut SolveApp, ui: &mut egui::Ui) { }); } +/// The SELECT panel: what a click in the view picks. +fn selection_filter(app: &mut SolveApp, ui: &mut egui::Ui) { + let t = Tokens::get(); + ui.label(egui::RichText::new("Selection filter").color(t.text_dim).size(11.0)); + ui.checkbox(&mut app.ui.pick_bodies, "Select bodies (a face picks its body)"); + for (k, label) in [("faces", "Faces"), ("edges", "Edges"), ("vertices", "Vertices"), ("sketch", "Sketch geometry and profiles")] { + let mut on = !app.ui.pick_off.iter().any(|x| x == k); + if ui.checkbox(&mut on, label).changed() { + app.ui.pick_off.retain(|x| x != k); + if !on { + app.ui.pick_off.push(k.to_string()); + } + } + } +} + /// A small drop-down caret. fn caret(p: &egui::Painter, c: egui::Pos2, col: Color32) { p.add(egui::Shape::convex_polygon(vec![pos2(c.x - 3.5, c.y - 2.0), pos2(c.x + 3.5, c.y - 2.0), pos2(c.x, c.y + 2.5)], col, Stroke::NONE)); diff --git a/crates/ui-egui/src/viewport.rs b/crates/ui-egui/src/viewport.rs index 50e0caa8..5e5d8fab 100644 --- a/crates/ui-egui/src/viewport.rs +++ b/crates/ui-egui/src/viewport.rs @@ -449,12 +449,20 @@ pub fn candidate(app: &SolveApp, hits: &[Hit]) -> Option<(Hit, Sel)> { if let Some(d) = app.dialog.as_ref().filter(|d| d.wants_picks()) { return hits.iter().find_map(|h| d.candidate(&app.session, h).map(|s| (h.clone(), s))); } + let off = |k: &str| app.ui.pick_off.iter().any(|x| x == k); hits.iter() .filter(|h| match h { - Hit::SketchCurve { sketch, .. } => app.session.active_sketch == Some(*sketch), - _ => true, + Hit::SketchCurve { sketch, .. } => app.session.active_sketch == Some(*sketch) && !off("sketch"), + Hit::SketchPoint { .. } | Hit::Profile { .. } => !off("sketch"), + Hit::Vertex { .. } => !off("vertices") && !app.ui.pick_bodies, + Hit::Edge { .. } => !off("edges") && !app.ui.pick_bodies, + Hit::Face { .. } => !off("faces") || app.ui.pick_bodies, + Hit::Plane { .. } | Hit::Axis { .. } => true, + }) + .find_map(|h| match h { + Hit::Face { body, .. } if app.ui.pick_bodies => Some((h.clone(), Sel::Body { name: body.clone() })), + _ => hit_sel(h).map(|s| (h.clone(), s)), }) - .find_map(|h| hit_sel(h).map(|s| (h.clone(), s))) } /// The edge a selection refers to now: by index if it still passes through the stored point, @@ -535,6 +543,7 @@ fn highlight_key(app: &SolveApp) -> u64 { serde_json::to_string(&app.highlighted()).unwrap_or_default().hash(&mut h); format!("{:?}", app.viewport.hover).hash(&mut h); app.viewport.hover_feature.hash(&mut h); + app.ui.pick_bodies.hash(&mut h); crate::theme::is_dark().hash(&mut h); app.preview.replaced.hash(&mut h); app.origin_visible().hash(&mut h); @@ -649,6 +658,17 @@ fn build_highlight(app: &SolveApp) -> GpuScene { } // Hover (pre-highlight). match hover { + // Body selection: the whole body lights up. + Some(Hit::Face { body, .. }) if app.ui.pick_bodies && app.dialog.is_none() && app.tool.is_none() => { + if let Some(b) = st.body(body) { + let k = t.hover_face_lift; + let c = (t.body.r(), t.body.g(), t.body.b()); + let m = b.mesh(); + for f in 0..b.body.face_count() { + face_tris(&mut sc, &m, f, [c.0.saturating_add(k), c.1.saturating_add(k), c.2.saturating_add(k), 255], true); + } + } + } Some(Hit::Face { body, index, .. }) if !app.preview.replaced.contains(body) && !sel.iter().any(|x| matches!(x, Sel::Face { body: b, index: i, .. } if b == body && i == index)) =>