Skip to main content

brep_app/panels/
selection.rs

1//! Selection panel — the **selection filter** (which entity kinds a viewport
2//! click may pick). Follows the panel pattern: a small state struct + a
3//! `show(&mut self, ui, state)` the shell calls once; `EngineState` stays the
4//! single brain (it owns the filter + the selection), borrowed in.
5//!
6//! * **Filter** — a toggle per kind (SOLID / FACE / EDGE / VERTEX). The engine
7//!   honors it in `select_top_at` (via `pick_filtered` with the enabled kinds),
8//!   so a plain click grabs only an allowed kind — a FACE-only filter selects a
9//!   face, a SOLID-only filter the owning solid. Defaults to ALL kinds enabled
10//!   (everything under the cursor is pickable, highest-priority kind wins).
11//!   Reference-selection mode temporarily constrains it to the active field's
12//!   allowed kinds (this panel is hidden then, but the constraint still governs
13//!   picking + hover). The model state is the engine's `selection_filter`; the
14//!   panel just reads/writes it.
15//!
16//! The quick actions on the current selection (Clear / Hide / Edit-owning-feature
17//! + the feature-from-selection actions) moved to the dedicated
18//! [`crate::panels::context_bar`] (the engine-native successor to the old app's
19//! floating selection action bar), which supersedes the minimal action bar this
20//! panel used to draw.
21//!
22//! The panel owns only the per-frame `hits` map (widget screen rects) the headed
23//! verifier reads to drive real clicks, exactly like the toolbar/history panels.
24
25use brep_render::engine_state::EngineState;
26use eframe::egui;
27use std::collections::HashMap;
28
29/// The pickable kinds, in the order the filter row draws them. `(key, label)`:
30/// the `key` is the engine kind name + the `hits` map key suffix.
31const KINDS: [(&str, &str); 4] = [
32    ("SOLID", "Solid"),
33    ("FACE", "Face"),
34    ("EDGE", "Edge"),
35    ("VERTEX", "Vertex"),
36];
37
38/// The selection panel's own state: the per-frame map of egui widget screen
39/// rects, published to JS for the headed verifier to drive real clicks. Rebuilt
40/// each frame (there is no DOM — egui is drawn on the canvas).
41#[derive(Default)]
42pub struct SelectionPanel {
43    hits: HashMap<String, egui::Rect>,
44}
45
46impl SelectionPanel {
47    pub fn new() -> Self {
48        Self::default()
49    }
50
51    /// Draw the selection filter. Rebuilds `hits` each frame as it draws.
52    pub fn show(&mut self, ui: &mut egui::Ui, state: &mut EngineState) {
53        self.hits.clear();
54        egui::CollapsingHeader::new("Selection")
55            .id_salt("selection")
56            .default_open(true)
57            .show(ui, |ui| {
58                self.filter_row(ui, state);
59            });
60    }
61
62    /// The published widget hit-rects (egui points) for the headed verifier —
63    /// `filter:SOLID|FACE|EDGE|VERTEX`.
64    #[cfg(target_arch = "wasm32")]
65    pub fn hits_json(&self) -> String {
66        let map: serde_json::Map<String, serde_json::Value> = self
67            .hits
68            .iter()
69            .map(|(k, r)| {
70                (
71                    k.clone(),
72                    serde_json::json!([r.min.x, r.min.y, r.width(), r.height()]),
73                )
74            })
75            .collect();
76        serde_json::Value::Object(map).to_string()
77    }
78
79    /// The filter toggles: one `toggle_value` per pickable kind, reflecting the
80    /// LIVE engine `selection_filter` so the row is always honest. On any change
81    /// the whole filter is written back through `set_selection_filter`.
82    fn filter_row(&mut self, ui: &mut egui::Ui, state: &mut EngineState) {
83        ui.label("Pickable kinds — a click selects the highest-priority one enabled:");
84        let mut filter = state.selection_filter();
85        let before = filter;
86        ui.horizontal_wrapped(|ui| {
87            for (kind, label) in KINDS {
88                let mut on = filter.get(kind);
89                let resp = ui.toggle_value(&mut on, label);
90                self.hits.insert(format!("filter:{kind}"), resp.rect);
91                if resp.changed() {
92                    filter.set(kind, on);
93                }
94            }
95        });
96        if filter != before {
97            state.set_selection_filter(filter);
98        }
99    }
100}