BREP_app 0.2.0

The BREP CAD application: an eframe (egui + wgpu) host that draws the brep-render 3D engine into an egui frame — native + wasm from one codebase.
Documentation
//! Selection panel — the **selection filter** (which entity kinds a viewport
//! click may pick), rendered as a single horizontal row in the shell's bottom
//! STATUS BAR (not a side-panel section). Follows the panel pattern: a small
//! state struct + a `show_status_bar(&mut self, ui, state)` the bottom bar calls
//! once per frame in modeling mode; `EngineState` stays the single brain (it owns
//! the filter + the selection), borrowed in.
//!
//! * **Filter** — a leading "All" tristate CHECKBOX followed by a real CHECKBOX
//!   per kind (COMPONENT / SOLID / FACE / EDGE / VERTEX / PLANE; COMPONENT =
//!   promote a pick on assembly-component geometry to the whole component;
//!   PLANE = the construction datum/plane cards, pickable like any face). The
//!   engine honors the filter in
//!   `select_top_at` (via the planes-aware `pick_top_at` with the enabled
//!   kinds), so a plain click grabs only an
//!   allowed kind — a FACE-only filter selects a face, a SOLID-only filter the
//!   owning solid, a PLANE-only filter the construction plane under the cursor. Defaults to ALL kinds enabled (everything under the cursor is
//!   pickable, highest-priority kind wins). Reference-selection mode temporarily
//!   constrains it to the active field's allowed kinds — while that picker is
//!   active the row is LOCKED (greyed + non-interactive) so a click can't
//!   overwrite the constraint. The model state is the engine's `selection_filter`;
//!   this panel just reads/writes it.
//!
//! The quick actions on the current selection (Clear / Hide / Edit-owning-feature
//! + the feature-from-selection actions) live in the dedicated
//! [`crate::panels::context_bar`] (the engine-native successor to the old app's
//! floating selection action bar).
//!
//! The panel owns only the per-frame `hits` map (widget screen rects) the headed
//! verifier reads to drive real clicks, exactly like the toolbar/history panels.

use brep_render::engine_state::{EngineState, SelectionFilter};
use eframe::egui;
use std::collections::HashMap;

/// The pickable kinds, in the order the filter row draws them: the geometry
/// kinds coarsest-first, then the CONSTRUCTION kind last. SKETCH sits beside
/// SOLID because a committed sketch is drawn as a sheet solid and picks as one;
/// the two lanes split that single pick kind so a sketch can be made pickable
/// (or not) independently of real bodies. It governs WHOLE sketches — a sketch's
/// face and its drawn segments stay under Face and Edge.
/// `(key, label)`: the `key` is the engine kind name + the `hits` map key
/// suffix; the `label` is the checkbox caption. COMPONENT is the promotion
/// kind: on, a pick landing on assembly-component geometry selects the WHOLE
/// component; off, the click reaches the sub-entity kinds. PLANE is the
/// construction kind: the drawn datum/plane cards, pickable like any face (they
/// rank right after faces in the pick list, so a plane under geometry is
/// reachable through the pick-list popup).
const KINDS: [(&str, &str); 7] = [
    ("COMPONENT", "Component"),
    ("SOLID", "Solid"),
    ("SKETCH", "Sketch"),
    ("FACE", "Face"),
    ("EDGE", "Edge"),
    ("VERTEX", "Vertex"),
    ("PLANE", "Plane"),
];

/// The selection panel's own state: the per-frame map of egui widget screen
/// rects, published to JS for the headed verifier to drive real clicks. Rebuilt
/// each frame (there is no DOM — egui is drawn on the canvas).
#[derive(Default)]
pub struct SelectionPanel {
    hits: HashMap<String, egui::Rect>,
}

impl SelectionPanel {
    pub fn new() -> Self {
        Self::default()
    }

    /// Draw the selection filter as a single horizontal row into the shell's
    /// bottom STATUS BAR: a leading "All" tristate checkbox + one CHECKBOX per
    /// pickable kind (all reflecting the LIVE engine `selection_filter`). Rebuilds
    /// `hits` as it draws. Called by the bottom bar in modeling mode.
    pub fn show_status_bar(&mut self, ui: &mut egui::Ui, state: &mut EngineState) {
        self.hits.clear();
        ui.horizontal_wrapped(|ui| {
            // While the reference-selection picker is active the engine
            // temporarily CONSTRAINS the filter to the active field's allowed
            // kinds (see `begin_ref_select`). Letting a bottom-bar click edit it
            // then would clobber that constraint, so LOCK the row: it still shows
            // the constrained kinds (greyed) but is non-interactive until the
            // picker finishes/cancels and the filter is restored.
            let locked = state.ref_select_active();
            if locked {
                ui.label(egui::RichText::new("\u{1f512} Reference selection — filter locked").weak())
                    .on_hover_text(
                        "The pick filter is set by the field being referenced. \
                         Finish or cancel the reference selection to change it.",
                    );
            } else {
                ui.label("Pickable:");
            }

            // `add_enabled_ui(false, …)` greys the widgets AND makes them
            // non-interactive, so no `.changed()`/`.clicked()` fires while locked
            // — the constrained filter cannot be overwritten. Rects are still laid
            // out and published so the verifier and layout stay consistent.
            ui.add_enabled_ui(!locked, |ui| {
                let mut filter = state.selection_filter();
                let before = filter;

                // Leading "All" TRISTATE CHECKBOX (mirrors the Scene tree's group
                // checkbox idiom): checked when EVERY kind is on, the indeterminate
                // dash when some-but-not-all are on, unchecked when none are.
                // Toggling it applies the toggle-all semantic — all-on → clear all;
                // partial or none → set all on — which is exactly the checkbox's
                // post-click value. Published under `filter:ALL` for the verifier.
                let all_on = KINDS.iter().all(|(k, _)| filter.get(k));
                let any_on = KINDS.iter().any(|(k, _)| filter.get(k));
                let mut all_checked = all_on;
                let resp = ui.add(
                    egui::Checkbox::new(&mut all_checked, "All").indeterminate(any_on && !all_on),
                );
                self.hits.insert("filter:ALL".into(), resp.rect);
                if resp.changed() {
                    let target = toggle_all_target(&filter);
                    for (kind, _) in KINDS {
                        filter.set(kind, target);
                    }
                }

                // One CHECKBOX per kind, reflecting the live filter. A `.changed()`
                // checkbox updates the working copy; it is written back once below.
                for (kind, label) in KINDS {
                    let mut on = filter.get(kind);
                    let resp = ui.checkbox(&mut on, label);
                    self.hits.insert(format!("filter:{kind}"), resp.rect);
                    if resp.changed() {
                        filter.set(kind, on);
                    }
                }

                if filter != before {
                    state.set_selection_filter(filter);
                }
            });
        });
    }

    /// Drop the published widget rects. The bottom bar calls this in sketch mode
    /// (when the filter row is NOT drawn) so the verifier never sees stale
    /// last-modeling-frame rects for widgets that are no longer on screen.
    pub fn clear_hits(&mut self) {
        self.hits.clear();
    }

    /// The published widget hit-rects (egui points) for the headed verifier —
    /// `filter:COMPONENT|SOLID|FACE|EDGE|VERTEX|PLANE` (the per-kind checkboxes)
    /// + `filter:ALL` (the leading "All" tristate checkbox).
    #[cfg(target_arch = "wasm32")]
    pub fn hits_json(&self) -> String {
        let map: serde_json::Map<String, serde_json::Value> = self
            .hits
            .iter()
            .map(|(k, r)| {
                (
                    k.clone(),
                    serde_json::json!([r.min.x, r.min.y, r.width(), r.height()]),
                )
            })
            .collect();
        serde_json::Value::Object(map).to_string()
    }
}

/// The "toggle all" target value: if EVERY pickable kind is currently on, the
/// "All" checkbox turns them all OFF; otherwise (some-but-not-all on, or none on)
/// it turns them all ON. Returns the value to write to every kind.
fn toggle_all_target(filter: &SelectionFilter) -> bool {
    !KINDS.iter().all(|(k, _)| filter.get(k))
}

#[cfg(test)]
mod tests {
    use super::*;
    use brep_render::engine_state::EngineState;

    /// Toggle-all semantics: all-on → all-off; any not-all state (partial OR
    /// none) → all-on. These are the exact rules the bottom-bar button applies.
    #[test]
    fn toggle_all_target_semantics() {
        // ALL on → target OFF.
        let all = SelectionFilter::default();
        assert!(
            all.solid && all.face && all.edge && all.vertex && all.plane && all.component,
            "default is all-on"
        );
        assert!(!toggle_all_target(&all), "all-on toggles to off");

        // PARTIAL (only face) → target ON.
        let partial = SelectionFilter {
            solid: false,
            sketch: false,
            face: true,
            edge: false,
            vertex: false,
            plane: false,
            component: false,
        };
        assert!(toggle_all_target(&partial), "partial toggles to on");

        // NONE on → target ON.
        let none = SelectionFilter {
            solid: false,
            sketch: false,
            face: false,
            edge: false,
            vertex: false,
            plane: false,
            component: false,
        };
        assert!(toggle_all_target(&none), "none toggles to on");
    }

    /// The status-bar row must publish a hit-rect for every checkbox
    /// (`filter:SOLID|FACE|EDGE|VERTEX`) plus the leading "All" tristate checkbox
    /// (`filter:ALL`) — the headed verifier drives them by these exact keys.
    #[test]
    fn status_bar_publishes_hit_rects() {
        let mut panel = SelectionPanel::new();
        let mut state = EngineState::new();
        let ctx = egui::Context::default();
        // One headless frame; layout records the per-frame hit-rects (no pointer
        // input → nothing is clicked).
        let _ = ctx.run_ui(egui::RawInput::default(), |ui| {
            panel.show_status_bar(ui, &mut state);
        });
        for key in [
            "filter:COMPONENT",
            "filter:SOLID",
            "filter:FACE",
            "filter:EDGE",
            "filter:VERTEX",
            "filter:PLANE",
            "filter:ALL",
        ] {
            assert!(
                panel.hits.contains_key(key),
                "status bar must publish `{key}`, got {:?}",
                panel.hits.keys().collect::<Vec<_>>()
            );
        }
    }

    /// A checkbox click drives the ENGINE filter through `set_selection_filter`:
    /// unchecking Face disables face-picking, rechecking re-enables it. Driven
    /// through the REAL egui widget via the accessibility tree (egui_kittest).
    #[test]
    fn checkbox_drives_engine_filter() {
        use egui_kittest::kittest::Queryable;
        let mut harness = egui_kittest::Harness::new_ui_state(
            |ui, (panel, engine): &mut (SelectionPanel, EngineState)| {
                panel.show_status_bar(ui, engine);
            },
            (SelectionPanel::new(), EngineState::new()),
        );
        harness.run();
        assert!(harness.state().1.selection_filter().face, "default: face pickable");

        harness.get_by_label("Face").click();
        harness.run();
        assert!(
            !harness.state().1.selection_filter().face,
            "unchecking Face must disable face-picking via set_selection_filter"
        );

        harness.get_by_label("Face").click();
        harness.run();
        assert!(
            harness.state().1.selection_filter().face,
            "rechecking Face must re-enable face-picking"
        );
    }

    /// The leading "All" tristate checkbox drives the toggle-all semantic through
    /// the ENGINE filter: from the all-on default a click clears every kind, and a
    /// second click sets them all back on. Driven through the REAL egui widget via
    /// the accessibility tree (egui_kittest).
    #[test]
    fn all_checkbox_toggles_every_kind() {
        use egui_kittest::kittest::Queryable;
        let mut harness = egui_kittest::Harness::new_ui_state(
            |ui, (panel, engine): &mut (SelectionPanel, EngineState)| {
                panel.show_status_bar(ui, engine);
            },
            (SelectionPanel::new(), EngineState::new()),
        );
        harness.run();
        let f = harness.state().1.selection_filter();
        assert!(
            f.solid && f.face && f.edge && f.vertex && f.plane && f.component,
            "default: all kinds on"
        );

        // All-on → clicking "All" clears every kind.
        harness.get_by_label("All").click();
        harness.run();
        let f = harness.state().1.selection_filter();
        assert!(
            !f.solid && !f.face && !f.edge && !f.vertex && !f.plane && !f.component,
            "clicking All while all-on must clear every kind: {f:?}"
        );

        // None-on → clicking "All" sets every kind.
        harness.get_by_label("All").click();
        harness.run();
        let f = harness.state().1.selection_filter();
        assert!(
            f.solid && f.face && f.edge && f.vertex && f.plane && f.component,
            "clicking All again must set every kind on: {f:?}"
        );
    }

    /// The PLANE checkbox drives the ENGINE filter exactly like the Face one:
    /// unchecking Plane disables construction-plane picking, rechecking re-enables
    /// it. Driven through the REAL egui widget via the accessibility tree
    /// (egui_kittest) — the same path a user's click takes.
    #[test]
    fn plane_checkbox_drives_engine_filter() {
        use egui_kittest::kittest::Queryable;
        let mut harness = egui_kittest::Harness::new_ui_state(
            |ui, (panel, engine): &mut (SelectionPanel, EngineState)| {
                panel.show_status_bar(ui, engine);
            },
            (SelectionPanel::new(), EngineState::new()),
        );
        harness.run();
        assert!(harness.state().1.selection_filter().plane, "default: plane pickable");

        harness.get_by_label("Plane").click();
        harness.run();
        let f = harness.state().1.selection_filter();
        assert!(
            !f.plane,
            "unchecking Plane must disable plane-picking via set_selection_filter"
        );
        assert!(f.face, "…and must not disturb the other kinds: {f:?}");

        harness.get_by_label("Plane").click();
        harness.run();
        assert!(
            harness.state().1.selection_filter().plane,
            "rechecking Plane must re-enable plane-picking"
        );
    }

    /// Reference-selection LOCK: while the picker is active the engine constrains
    /// the filter to the field's allowed kinds; the bottom-bar row must be locked
    /// so a click cannot overwrite that constraint. Driven through the REAL egui
    /// widget (egui_kittest): clicking a checkbox while locked is a no-op.
    #[test]
    fn ref_select_locks_the_filter_row() {
        use egui_kittest::kittest::Queryable;
        let mut state = EngineState::new();
        // Activate ref-select for a FACE-only field. An absent feature id just
        // resolves the "before" step to the current one — enough to flip
        // `ref_select_active()` on and constrain the filter to face-only.
        state.begin_ref_select(
            "f",
            vec!["p".into()],
            "Ref".into(),
            vec!["FACE".into()],
            false,
            vec![],
        );
        assert!(state.ref_select_active(), "ref-select is active");
        let constrained = state.selection_filter();
        assert!(
            constrained.face && !constrained.solid,
            "field filter constrained to face-only: {constrained:?}"
        );

        let mut harness = egui_kittest::Harness::new_ui_state(
            |ui, (panel, engine): &mut (SelectionPanel, EngineState)| {
                panel.show_status_bar(ui, engine);
            },
            (SelectionPanel::new(), state),
        );
        harness.run();
        // Try to enable Solid: the row is locked (disabled), so the click is a
        // no-op and the constrained filter is left exactly as the engine set it.
        harness.get_by_label("Solid").click();
        harness.run();
        assert_eq!(
            harness.state().1.selection_filter(),
            constrained,
            "a locked checkbox click must not change the ref-select-constrained filter"
        );
    }
}