Skip to main content

brep_app/panels/
scene.rs

1//! Scene panel — the engine-native **Scene tree** ("Scene Manager"), the second
2//! sidebar tree in the design reference. Built on the SAME reusable [`tree`] node
3//! helper (connector lines + `[+]`/`[-]` collapse boxes) the history panel uses,
4//! so the two trees read as one system.
5//!
6//! # What it draws
7//!
8//! Above the tree sits a **type-visibility button row** (`Faces` / `Edges` /
9//! `Vertices`, plus `Sketches` / `Planes` when the scene has them): each button
10//! hides/shows EVERY object of that type SCENE-WIDE in one click — the missing
11//! bulk complement to the per-solid group tristate below. The toggle follows the
12//! same rule as the group checkbox + selection-filter toggle-all: if EVERY object
13//! of that type is currently visible → hide them all; otherwise (some or all
14//! hidden) → show them all. There is deliberately no `Solids` button — the Scene
15//! ROOT checkbox already toggles every solid.
16//!
17//! A file-tree of the engine's display scene:
18//!   * `[-] Scene <☑>` — the root; its checkbox toggles ALL solids' visibility.
19//!   * per solid `[-] <Name> <☑>` — the checkbox toggles that solid's visibility
20//!     through the engine ([`EngineState::set_visible`]); expands to
21//!   * `[+] Faces <☑>`, `[+] Edges <☑>`, `[+] Vertices <☑>` — each expands to the
22//!     individual entities BY KERNEL NAME (vertices by index/position).
23//!
24//! # Selection sync (both ways)
25//!
26//! Clicking an entity row drives the engine's name-based SELECTION
27//! ([`EngineState::select_by_name`] / [`select_vertex_by_position`]) so it
28//! highlights (emphasis) in the viewport; and the engine's CURRENT selection
29//! (`state.emphasis`) bolds the matching tree row — the same emphasis the
30//! viewport reads, so a viewport pick lights up the tree and vice-versa.
31//!
32//! # Hover sync (row → viewport)
33//!
34//! MOUSING OVER a row highlights that entity in the 3D view exactly as mousing
35//! over it in the viewport does: the row feeds the SAME `emphasis` hover buckets
36//! the viewport's `hover_at` fills, through the name-based twins
37//! ([`EngineState::hover_by_name`] / [`hover_vertex_by_position`]) — so there is
38//! one hover highlight, one style, one clear. It lights exactly the rows whose
39//! CLICK selects one 3D entity (solids, committed sketches, datums/planes, and
40//! the face/edge/vertex leaves); the Scene root and the Faces/Edges/Vertices
41//! GROUP nodes are not things in the 3D view, so they stay unlit. The pointer is
42//! off the viewport while it is over a row, so the engine's one-frame
43//! [`EngineState::take_scene_tree_hover`] flag makes the viewport's hover pass
44//! yield instead of clearing the row's highlight.
45//!
46//! # This panel OWNS NO model state
47//!
48//! The scene + selection live in the engine ([`EngineState`], the single source
49//! of truth). The panel holds only transient UI state: which nodes are expanded
50//! and the per-frame `hits` map (widget screen rects) the headed verifier reads.
51//! Each frame it snapshots the scene into owned rows FIRST, draws from that, and
52//! applies at most one deferred engine mutation after the draw loop (so no borrow
53//! of `state` is held across a `&mut` call — the history panel's pattern).
54//!
55//! # Two deliberate reshapes (functional-over-1:1, per the design doc)
56//!
57//! * The visibility checkbox is drawn in the row's RIGHT slot: the shared tree
58//!   widget reserves the left columns for the collapse box + connector + glyph,
59//!   and (per the constraints) it is reused verbatim, not modified. Functionally
60//!   identical to the reference's left checkbox.
61//! * **Per-entity visibility (live):** the engine now hides individual faces /
62//!   edges / vertices and whole groups ([`EngineState::set_entity_visible`] /
63//!   [`EngineState::set_group_visible`]). So each Faces/Edges/Vertices group
64//!   checkbox is a live TRISTATE (all / some / none of that kind shown) and each
65//!   entity leaf carries its own live checkbox — the render pass skips a hidden
66//!   entity's triangles / segments / point. The whole-solid + whole-scene
67//!   checkboxes still compose: a hidden solid draws nothing; re-showing it keeps
68//!   any per-entity hides intact.
69
70use crate::automation::hit_keys::HitKeyDoc;
71use crate::panels::toolbar_button;
72use crate::panels::tree::{self, TreeRow};
73use brep_render::engine_state::EngineState;
74use brep_render::visibility::{EntityKind as VisKind, GroupState};
75use eframe::egui;
76use std::collections::{HashMap, HashSet};
77
78/// One entity leaf's identity — how a click maps to an engine selection call.
79#[derive(Clone)]
80enum EntityKind {
81    /// Face, selected by kernel name (empty = unnamed → not selectable).
82    Face(String),
83    /// Edge, selected by kernel name (empty = unnamed → not selectable).
84    Edge(String),
85    /// Vertex, selected by owning-solid + world position (no kernel name).
86    Vertex([f64; 3]),
87}
88
89/// One row under a Faces/Edges/Vertices group — a display label, its selection
90/// identity, whether it is currently in the engine selection, and whether it is
91/// currently VISIBLE in the engine (its live per-entity checkbox state).
92#[derive(Clone)]
93struct Entity {
94    label: String,
95    kind: EntityKind,
96    selected: bool,
97    visible: bool,
98}
99
100/// One solid's owned snapshot for the frame (decoupled from `state.scene` so the
101/// draw loop can issue deferred `&mut state` mutations afterwards).
102struct SolidRow {
103    name: String,
104    visible: bool,
105    selected: bool,
106    faces: Vec<Entity>,
107    edges: Vec<Entity>,
108    vertices: Vec<Entity>,
109}
110
111/// A deferred engine mutation, collected during the draw and applied once after
112/// the loop (one per frame — the history panel's pattern).
113enum Action {
114    SetVisible(String, bool),
115    SetAllVisible(bool),
116    /// Hide/show one entity: `(solid, kind, index-in-kind-list, visible)`.
117    SetEntityVisible(String, VisKind, usize, bool),
118    /// Hide/show a whole group: `(solid, kind, visible)`.
119    SetGroupVisible(String, VisKind, bool),
120    /// Hide/show one group KIND across EVERY solid in the scene: `(kind, visible)`.
121    SetAllGroupVisible(VisKind, bool),
122    /// Hide/show EVERY committed sketch's overlay: `(visible)`.
123    SetAllSketchVisible(bool),
124    /// Hide/show EVERY construction datum/plane: `(visible)`.
125    SetAllDatumVisible(bool),
126    Select(&'static str, String),
127    SelectVertex(String, [f64; 3]),
128    /// Show/hide a committed sketch's persistent overlay: `(feature-id, visible)`.
129    SetSketchVisible(String, bool),
130    /// Show/hide a construction datum/plane's plane: `(frame-name, visible)`.
131    SetDatumVisible(String, bool),
132    /// Select a construction datum/plane by frame NAME (a row click).
133    SelectDatum(String),
134}
135
136/// The row the pointer is over this frame — the hover twin of [`Action`]'s
137/// select arms, and deliberately the SAME targets: a row hover previews exactly
138/// what that row's click would select. Collected during the draw, applied once
139/// after it (like [`Action`], so no borrow of `state` is held across the loop).
140enum Hover {
141    /// Solid / committed sketch / face / edge / datum — by kernel (or frame) NAME.
142    Named(&'static str, String),
143    /// A vertex: owning solid + world position (vertices carry no kernel name).
144    Vertex(String, [f64; 3]),
145}
146
147/// The Scene tree panel's transient UI state (the scene + selection live in the
148/// engine).
149#[derive(Default)]
150pub struct ScenePanel {
151    /// Per-frame egui widget screen rects, published to JS for the headed
152    /// verifier. Rebuilt every frame.
153    hits: HashMap<String, egui::Rect>,
154    /// The Scene ROOT is collapsed (absent/false = open — it defaults open).
155    root_collapsed: bool,
156    /// Solids explicitly COLLAPSED, by name (absent = open — solids default open,
157    /// matching the reference showing a solid's Faces/Edges/Vertices).
158    collapsed_solids: HashSet<String>,
159    /// Faces/Edges/Vertices group nodes explicitly EXPANDED, keyed
160    /// `"<solid>/<group>"` (absent = collapsed — groups default collapsed `[+]`).
161    expanded_groups: HashSet<String>,
162    /// The `Planes & Datums` group node is collapsed (absent/false = open — it
163    /// defaults open so construction datums are visible in the tree).
164    datums_collapsed: bool,
165}
166
167impl ScenePanel {
168    pub fn new() -> Self {
169        Self::default()
170    }
171
172    /// Draw the Scene tree. Snapshots the scene + current selection into owned
173    /// rows, draws them via the shared [`tree`] node helper, then applies at most
174    /// one deferred engine mutation.
175    pub fn show(&mut self, ui: &mut egui::Ui, state: &mut EngineState) {
176        self.hits.clear();
177
178        // --- snapshot scene + selection (owned) so we can mutate after drawing --
179        let sel_solids = state.emphasis.selected_solids.clone();
180        let sel_faces = state.emphasis.selected_faces.clone();
181        let sel_edges = state.emphasis.selected_edges.clone();
182        let sel_vertices = state.emphasis.selected_vertices.clone();
183        let solids = snapshot(state, &sel_solids, &sel_faces, &sel_edges, &sel_vertices);
184        // Committed sketches (id, visible) — listed under the solids in the tree.
185        let sketches = state.committed_sketches();
186        // Construction datums/planes (name, visible) + which are selected — listed as
187        // the tree's LAST group.
188        let sel_datums = state.emphasis.selected_datums.clone();
189        let datums = state.construction_datums();
190
191        // Tight, tree-like row spacing so connector verticals read continuously.
192        ui.spacing_mut().item_spacing.y = 2.0;
193
194        let mut action: Option<Action> = None;
195        // The row under the pointer (at most one — rows don't overlap).
196        let mut hover: Option<Hover> = None;
197
198        // --- TYPE-VISIBILITY button row (scene-wide, ABOVE the tree) -----------
199        // One button per display TYPE; each hides/shows EVERY object of that type
200        // across the whole scene. Same toggle rule as the per-solid group tristate
201        // + the selection-filter toggle-all: all-visible → hide all; otherwise
202        // (some or all hidden) → show all. Buttons show a "pressed" (selected) look
203        // when all of that type are currently visible. There is no `Solids` button
204        // — the Scene root checkbox already toggles every solid.
205        ui.horizontal(|ui| {
206            let has_solids = !solids.is_empty();
207
208            // Faces / Edges / Vertices — scene-wide across ALL solids. All-visible
209            // means every solid reports GroupState::All for that kind.
210            let groups: [(VisKind, &str, bool); 3] = [
211                (
212                    VisKind::Face,
213                    "Faces",
214                    has_solids
215                        && solids
216                            .iter()
217                            .all(|s| matches!(group_state(&s.faces), GroupState::All)),
218                ),
219                (
220                    VisKind::Edge,
221                    "Edges",
222                    has_solids
223                        && solids
224                            .iter()
225                            .all(|s| matches!(group_state(&s.edges), GroupState::All)),
226                ),
227                (
228                    VisKind::Vertex,
229                    "Vertices",
230                    has_solids
231                        && solids
232                            .iter()
233                            .all(|s| matches!(group_state(&s.vertices), GroupState::All)),
234                ),
235            ];
236            for (kind, label, all_visible) in groups {
237                let tip = if all_visible {
238                    format!("Hide all {}", label.to_lowercase())
239                } else {
240                    format!("Show all {}", label.to_lowercase())
241                };
242                // Disabled (greyed + non-interactive) with no solids, matching the
243                // Scene root checkbox's `add_enabled(!solids.is_empty(), …)` spirit.
244                let resp = ui
245                    .add_enabled_ui(has_solids, |ui| {
246                        toolbar_button::toggle(ui, all_visible, label, &tip)
247                    })
248                    .inner;
249                self.hits.insert(format!("typevis:{label}"), resp.rect);
250                if resp.clicked() {
251                    action = Some(Action::SetAllGroupVisible(kind, !all_visible));
252                }
253            }
254
255            // Sketches — shown only when the scene has committed sketches (mirrors
256            // `render_sketches` being conditional). All-visible = every sketch shown.
257            if !sketches.is_empty() {
258                let all_visible = sketches.iter().all(|(_, v)| *v);
259                let tip = if all_visible {
260                    "Hide all sketches"
261                } else {
262                    "Show all sketches"
263                };
264                let resp = toolbar_button::toggle(ui, all_visible, "Sketches", tip);
265                self.hits.insert("typevis:Sketches".into(), resp.rect);
266                if resp.clicked() {
267                    action = Some(Action::SetAllSketchVisible(!all_visible));
268                }
269            }
270
271            // Planes & Datums — shown only when the scene has construction datums
272            // (mirrors `render_datums` being conditional). Hit key is the literal
273            // `typevis:Datums` the verifier drives; the button LABEL reads "Planes".
274            if !datums.is_empty() {
275                let all_visible = datums.iter().all(|(_, v)| *v);
276                let tip = if all_visible {
277                    "Hide all planes & datums"
278                } else {
279                    "Show all planes & datums"
280                };
281                let resp = toolbar_button::toggle(ui, all_visible, "Planes", tip);
282                self.hits.insert("typevis:Datums".into(), resp.rect);
283                if resp.clicked() {
284                    action = Some(Action::SetAllDatumVisible(!all_visible));
285                }
286            }
287        });
288
289        // --- ROOT: `[-] Scene  ☑` (visibility toggles every solid) ------------
290        let root_open = !self.root_collapsed;
291        let all_visible = !solids.is_empty() && solids.iter().all(|s| s.visible);
292        let mut root_vis = all_visible;
293        let mut root_vis_rect = egui::Rect::NOTHING;
294        let mut root_vis_clicked = false;
295        let root_resp = tree::node(
296            ui,
297            TreeRow {
298                guides: &[],
299                is_last: true,
300                expandable: true,
301                expanded: root_open,
302                root: true,
303                glyph: None,
304                label: "Scene",
305                selected: false,
306                draggable: false,
307                tint: None,
308            },
309            |ui| {
310                let cb = ui.add_enabled(!solids.is_empty(), egui::Checkbox::new(&mut root_vis, ""));
311                root_vis_rect = cb.rect;
312                root_vis_clicked = cb.clicked();
313            },
314        );
315        self.hits.insert("box:__scene".into(), root_resp.box_rect);
316        self.hits.insert("vis:__scene".into(), root_vis_rect);
317        if root_vis_clicked {
318            action = Some(Action::SetAllVisible(root_vis));
319        }
320        if root_resp.toggled || root_resp.label.clicked() {
321            self.root_collapsed = !self.root_collapsed;
322        }
323
324        if solids.is_empty() && sketches.is_empty() && datums.is_empty() {
325            let g = tree::child_guides(&[], true);
326            tree::node(ui, TreeRow::leaf(&g, true, "(scene is empty)"), |_| {});
327        }
328
329        if root_open {
330            // Top-level children under the root, in order: solids, then each committed
331            // sketch as its OWN top-level row (no group wrapper — a committed sketch is
332            // a scene solid, listed like the solids), then the `Planes & Datums` group
333            // (the last child), so nothing "below" is marked last while a later child
334            // still follows.
335            let has_sketches = !sketches.is_empty();
336            let has_datums = !datums.is_empty();
337            let n = solids.len();
338            for (si, solid) in solids.iter().enumerate() {
339                let is_last = !has_sketches && !has_datums && si + 1 == n;
340                self.render_solid(ui, solid, is_last, &mut action, &mut hover);
341            }
342            let m = sketches.len();
343            for (i, (id, visible)) in sketches.iter().enumerate() {
344                let is_last = !has_datums && i + 1 == m;
345                let selected = sel_solids.contains(id);
346                self.render_sketch_row(ui, id, *visible, selected, is_last, &mut action, &mut hover);
347            }
348            if has_datums {
349                self.render_datums(ui, &datums, &sel_datums, &mut action, &mut hover);
350            }
351        }
352
353        // --- apply the one deferred engine mutation ---------------------------
354        match action {
355            Some(Action::SetVisible(name, v)) => {
356                state.set_visible(&name, v);
357            }
358            Some(Action::SetAllVisible(v)) => {
359                for s in &solids {
360                    state.set_visible(&s.name, v);
361                }
362            }
363            Some(Action::SetEntityVisible(name, kind, index, v)) => {
364                state.set_entity_visible(&name, kind, index, v);
365            }
366            Some(Action::SetGroupVisible(name, kind, v)) => {
367                state.set_group_visible(&name, kind, v);
368            }
369            Some(Action::SetAllGroupVisible(kind, v)) => {
370                for s in &solids {
371                    state.set_group_visible(&s.name, kind, v);
372                }
373            }
374            Some(Action::SetAllSketchVisible(v)) => {
375                for (id, _) in &sketches {
376                    state.set_sketch_visible(id, v);
377                }
378            }
379            Some(Action::SetAllDatumVisible(v)) => {
380                for (name, _) in &datums {
381                    state.set_datum_visible(name, v);
382                }
383            }
384            Some(Action::Select(kind, name)) => {
385                state.select_by_name(kind, &name);
386            }
387            Some(Action::SelectVertex(solid, pos)) => {
388                state.select_vertex_by_position(&solid, pos);
389            }
390            Some(Action::SetSketchVisible(id, v)) => {
391                state.set_sketch_visible(&id, v);
392            }
393            Some(Action::SetDatumVisible(name, v)) => {
394                state.set_datum_visible(&name, v);
395            }
396            Some(Action::SelectDatum(name)) => {
397                state.select_datum(&name);
398            }
399            None => {}
400        }
401
402        // --- apply the row HOVER (row → viewport highlight) --------------------
403        // Independent of the click above (hovering and clicking legitimately land
404        // on the same frame) and applied EVERY frame: the engine dedupes a held
405        // hover and re-arms the one-frame flag the viewport's hover pass yields to,
406        // and `scene_tree_hover_end` is a no-op once it has cleared what the tree
407        // lit (it never touches a hover the viewport set).
408        let hover_changed = match &hover {
409            Some(Hover::Named(kind, name)) => state.hover_by_name(kind, name),
410            Some(Hover::Vertex(solid, position)) => {
411                state.hover_vertex_by_position(solid, *position)
412            }
413            None => state.scene_tree_hover_end(),
414        };
415        if hover_changed {
416            // The viewport tile may have drawn (and consumed `state.dirty`) BEFORE
417            // this pane in the dock, so without this the new highlight would wait
418            // for the next pointer event to reach the screen.
419            ui.ctx().request_repaint();
420        }
421
422        // --- verifier hooks (wasm only): scene listing + widget hit-rects ------
423        // Published from the panel (not the shared shell) so the headed verifier
424        // can assert the tree contents / visibility and drive real clicks, without
425        // touching `app.rs`'s shared publish block.
426        if crate::automation::registry::enabled() {
427            crate::automation::registry::publish("__brepScene", "scene tree solids with faces, edges, vertices", &state.scene_entities_json());
428            crate::automation::registry::publish("__brepSketches", "committed sketches", &state.sketch_entities_json());
429            crate::automation::registry::publish("__brepDatums", "datum entities", &state.datum_entities_json());
430            crate::automation::registry::publish("__brepSceneVis", "scene visibility map", &state.scene_visibility_json());
431            crate::automation::registry::publish("__brepSceneHit", "scene tree widget rects (box:, sel:, vis:, typevis:)", &self.hits_json());
432        }
433    }
434
435    /// One committed sketch as a TOP-LEVEL leaf row (no group wrapper): a
436    /// visibility checkbox wired to
437    /// [`EngineState::set_sketch_visible`](brep_render::engine_state::EngineState::set_sketch_visible)
438    /// and a label that SELECTS the sketch's sheet solid on click (a committed
439    /// sketch is a scene solid, dim-cyan / `is_sketch`-styled in the viewport, kept
440    /// OUT of the plain-solid rows by the [`snapshot`] filter so it lists exactly
441    /// once). Rendered under the solids, before the `Planes & Datums` group;
442    /// `is_last` is set only when it is the final top-level child.
443    fn render_sketch_row(
444        &mut self,
445        ui: &mut egui::Ui,
446        id: &str,
447        visible: bool,
448        selected: bool,
449        is_last: bool,
450        action: &mut Option<Action>,
451        hover: &mut Option<Hover>,
452    ) {
453        let mut vis = visible;
454        let mut vis_rect = egui::Rect::NOTHING;
455        let mut vis_clicked = false;
456        let resp = tree::node(
457            ui,
458            TreeRow::leaf(&[], is_last, id).selected(selected),
459            |ui| {
460                let cb = ui.add(egui::Checkbox::new(&mut vis, ""));
461                vis_rect = cb.rect;
462                vis_clicked = cb.clicked();
463            },
464        );
465        self.hits.insert(format!("vis:sketch/{id}"), vis_rect);
466        self.hits.insert(format!("sel:sketch/{id}"), resp.label.rect);
467        if vis_clicked {
468            *action = Some(Action::SetSketchVisible(id.to_string(), vis));
469        } else if resp.label.clicked() {
470            // A committed sketch is a scene solid — select it like any solid.
471            *action = Some(Action::Select("solid", id.to_string()));
472        }
473        if resp.label.hovered() {
474            *hover = Some(Hover::Named("solid", id.to_string()));
475        }
476    }
477
478    /// The `Planes & Datums` group node + (when open) one row per construction
479    /// datum/plane frame, each with a visibility checkbox wired to
480    /// [`EngineState::set_datum_visible`](brep_render::engine_state::EngineState::set_datum_visible)
481    /// and a label that selects the datum
482    /// ([`EngineState::select_datum`](brep_render::engine_state::EngineState::select_datum))
483    /// on click. Rendered as the Scene root's LAST child (only when there is at
484    /// least one construction datum).
485    fn render_datums(
486        &mut self,
487        ui: &mut egui::Ui,
488        datums: &[(String, bool)],
489        sel_datums: &HashSet<String>,
490        action: &mut Option<Action>,
491        hover: &mut Option<Hover>,
492    ) {
493        let open = !self.datums_collapsed;
494        let resp = tree::node(
495            ui,
496            TreeRow::branch(&[], true, open, "Planes & Datums"),
497            |ui| {
498                ui.add_space(6.0);
499                ui.label(egui::RichText::new(format!("{}", datums.len())).weak());
500            },
501        );
502        self.hits.insert("box:__datums".into(), resp.box_rect);
503        if resp.toggled || resp.label.clicked() {
504            self.datums_collapsed = !self.datums_collapsed;
505        }
506        if !open {
507            return;
508        }
509
510        let base = tree::child_guides(&[], true);
511        let m = datums.len();
512        for (i, (name, visible)) in datums.iter().enumerate() {
513            let last = i + 1 == m;
514            let selected = sel_datums.contains(name);
515            let mut vis = *visible;
516            let mut vis_rect = egui::Rect::NOTHING;
517            let mut vis_clicked = false;
518            let resp = tree::node(
519                ui,
520                TreeRow::leaf(&base, last, name).selected(selected),
521                |ui| {
522                    let cb = ui.add(egui::Checkbox::new(&mut vis, ""));
523                    vis_rect = cb.rect;
524                    vis_clicked = cb.clicked();
525                },
526            );
527            self.hits.insert(format!("vis:datum/{name}"), vis_rect);
528            self.hits.insert(format!("sel:datum/{name}"), resp.label.rect);
529            if vis_clicked {
530                *action = Some(Action::SetDatumVisible(name.clone(), vis));
531            } else if resp.label.clicked() {
532                *action = Some(Action::SelectDatum(name.clone()));
533            }
534            if resp.label.hovered() {
535                *hover = Some(Hover::Named("datum", name.clone()));
536            }
537        }
538    }
539
540    /// One solid node + (when open) its Faces / Edges / Vertices groups.
541    fn render_solid(
542        &mut self,
543        ui: &mut egui::Ui,
544        solid: &SolidRow,
545        is_last: bool,
546        action: &mut Option<Action>,
547        hover: &mut Option<Hover>,
548    ) {
549        let name = &solid.name;
550        let open = !self.collapsed_solids.contains(name);
551
552        let mut vis = solid.visible;
553        let mut vis_rect = egui::Rect::NOTHING;
554        let mut vis_clicked = false;
555        let resp = tree::node(
556            ui,
557            TreeRow::branch(&[], is_last, open, name).selected(solid.selected),
558            |ui| {
559                let cb = ui.add(egui::Checkbox::new(&mut vis, ""));
560                vis_rect = cb.rect;
561                vis_clicked = cb.clicked();
562            },
563        );
564        self.hits.insert(format!("box:{name}"), resp.box_rect);
565        self.hits.insert(format!("vis:{name}"), vis_rect);
566        self.hits.insert(format!("sel:{name}"), resp.label.rect);
567
568        if vis_clicked {
569            *action = Some(Action::SetVisible(name.clone(), vis));
570        }
571        if resp.toggled {
572            if open {
573                self.collapsed_solids.insert(name.clone());
574            } else {
575                self.collapsed_solids.remove(name);
576            }
577        }
578        if resp.label.clicked() {
579            *action = Some(Action::Select("solid", name.clone()));
580        }
581        if resp.label.hovered() {
582            *hover = Some(Hover::Named("solid", name.clone()));
583        }
584
585        if open {
586            let base = tree::child_guides(&[], is_last);
587            self.render_group(ui, name, &base, false, "Faces", VisKind::Face, &solid.faces, action, hover);
588            self.render_group(ui, name, &base, false, "Edges", VisKind::Edge, &solid.edges, action, hover);
589            self.render_group(ui, name, &base, true, "Vertices", VisKind::Vertex, &solid.vertices, action, hover);
590        }
591    }
592
593    /// One Faces/Edges/Vertices group node + (when open) its entity leaves. The
594    /// group checkbox is a live TRISTATE that hides/shows every entity of `kind`;
595    /// each leaf carries its own live checkbox that hides just that entity.
596    #[allow(clippy::too_many_arguments)]
597    fn render_group(
598        &mut self,
599        ui: &mut egui::Ui,
600        solid_name: &str,
601        base: &[bool],
602        is_last: bool,
603        group: &str,
604        kind: VisKind,
605        entities: &[Entity],
606        action: &mut Option<Action>,
607        hover: &mut Option<Hover>,
608    ) {
609        let key = format!("{solid_name}/{group}");
610        let open = self.expanded_groups.contains(&key);
611
612        // Tristate over the group's entities (empty group reads All → checked).
613        let state = group_state(entities);
614        let mut checked = matches!(state, GroupState::All);
615        let indeterminate = matches!(state, GroupState::Partial);
616        let mut vis_rect = egui::Rect::NOTHING;
617        let mut vis_clicked = false;
618        let resp = tree::node(
619            ui,
620            TreeRow::branch(base, is_last, open, group),
621            |ui| {
622                // right-to-left: the tristate group checkbox (rightmost), then count.
623                let cb = ui.add(
624                    egui::Checkbox::new(&mut checked, "").indeterminate(indeterminate),
625                );
626                vis_rect = cb.rect;
627                vis_clicked = cb.clicked();
628                ui.add_space(6.0);
629                ui.label(egui::RichText::new(format!("{}", entities.len())).weak());
630            },
631        );
632        self.hits.insert(format!("box:{key}"), resp.box_rect);
633        self.hits.insert(format!("vis:{key}"), vis_rect);
634        if vis_clicked {
635            // Standard tristate: All → hide all; None/Partial → show all.
636            let want_visible = !matches!(state, GroupState::All);
637            *action = Some(Action::SetGroupVisible(solid_name.to_string(), kind, want_visible));
638        }
639        if resp.toggled || resp.label.clicked() {
640            if open {
641                self.expanded_groups.remove(&key);
642            } else {
643                self.expanded_groups.insert(key.clone());
644            }
645        }
646
647        if !open {
648            return;
649        }
650        let gg = tree::child_guides(base, is_last);
651        if entities.is_empty() {
652            tree::node(ui, TreeRow::leaf(&gg, true, "(none)"), |_| {});
653            return;
654        }
655        let m = entities.len();
656        for (ei, e) in entities.iter().enumerate() {
657            let last = ei + 1 == m;
658            let mut vis = e.visible;
659            let mut ev_rect = egui::Rect::NOTHING;
660            let mut ev_clicked = false;
661            let resp = tree::node(
662                ui,
663                TreeRow::leaf(&gg, last, &e.label).selected(e.selected),
664                |ui| {
665                    let cb = ui.add(egui::Checkbox::new(&mut vis, ""));
666                    ev_rect = cb.rect;
667                    ev_clicked = cb.clicked();
668                },
669            );
670            self.hits.insert(format!("sel:{key}/{ei}"), resp.label.rect);
671            self.hits.insert(format!("vis:{key}/{ei}"), ev_rect);
672            if ev_clicked {
673                *action = Some(Action::SetEntityVisible(solid_name.to_string(), kind, ei, vis));
674            } else if resp.label.clicked() {
675                *action = Some(match &e.kind {
676                    EntityKind::Face(n) => Action::Select("face", n.clone()),
677                    EntityKind::Edge(n) => Action::Select("edge", n.clone()),
678                    EntityKind::Vertex(p) => Action::SelectVertex(solid_name.to_string(), *p),
679                });
680            }
681            if resp.label.hovered() {
682                // An UNNAMED face/edge has nothing to highlight (it is not
683                // selectable either) — leaving `hover` unset clears instead, so a
684                // neighbouring row's highlight never lingers under the pointer.
685                *hover = match &e.kind {
686                    EntityKind::Face(n) if !n.is_empty() => {
687                        Some(Hover::Named("face", n.clone()))
688                    }
689                    EntityKind::Edge(n) if !n.is_empty() => {
690                        Some(Hover::Named("edge", n.clone()))
691                    }
692                    EntityKind::Vertex(p) => {
693                        Some(Hover::Vertex(solid_name.to_string(), *p))
694                    }
695                    _ => None,
696                };
697            }
698        }
699    }
700
701    /// The published widget hit-rects (egui points) for the headed verifier.
702    pub fn hits_json(&self) -> String {
703        crate::automation::hit_rects::hits_json(&self.hits)
704    }
705}
706
707/// The tristate for a group of entity rows, computed from their live per-entity
708/// `visible` flags (the same the engine would report). An empty group reads
709/// [`GroupState::All`] — nothing to hide, so the checkbox shows checked.
710fn group_state(entities: &[Entity]) -> GroupState {
711    if entities.is_empty() {
712        return GroupState::All;
713    }
714    let visible = entities.iter().filter(|e| e.visible).count();
715    if visible == entities.len() {
716        GroupState::All
717    } else if visible == 0 {
718        GroupState::None
719    } else {
720        GroupState::Partial
721    }
722}
723
724/// Snapshot `state.scene` into owned rows, precomputing each entity's `selected`
725/// flag against the current engine selection (cloned in by the caller) and its
726/// live per-entity `visible` flag. Face / edge labels fall back to `Face i` /
727/// `Edge i` when the kernel left them unnamed; unnamed entities are then not
728/// name-selectable (the click is a no-op) but still hideable (by index).
729fn snapshot(
730    state: &EngineState,
731    sel_solids: &HashSet<String>,
732    sel_faces: &HashSet<String>,
733    sel_edges: &HashSet<String>,
734    sel_vertices: &[brep_render::style::VertexRef],
735) -> Vec<SolidRow> {
736    // Vertex positions are set exactly from the same source; a tiny tolerance
737    // guards float round-trips.
738    const TOL: f64 = 1e-6;
739    state
740        .scene
741        .solids()
742        .iter()
743        // Committed-sketch SHEETS are scene solids too, but they list as their OWN
744        // top-level sketch rows (`render_sketch_row`, checkbox wired to
745        // `set_sketch_visible`) — never as plain solid rows, so they are dropped here
746        // and never counted by the root / type-visibility toggles.
747        .filter(|s| !s.is_sketch)
748        .map(|s| {
749            let faces = s
750                .faces
751                .iter()
752                .enumerate()
753                .map(|(i, f)| Entity {
754                    label: if f.name.is_empty() {
755                        format!("Face {i}")
756                    } else {
757                        f.name.clone()
758                    },
759                    selected: !f.name.is_empty() && sel_faces.contains(&f.name),
760                    visible: s.visibility.is_face_visible(i),
761                    kind: EntityKind::Face(f.name.clone()),
762                })
763                .collect();
764            let edges = s
765                .edges
766                .iter()
767                .enumerate()
768                .map(|(i, e)| Entity {
769                    label: if e.name.is_empty() {
770                        format!("Edge {i}")
771                    } else {
772                        e.name.clone()
773                    },
774                    selected: !e.name.is_empty() && sel_edges.contains(&e.name),
775                    visible: s.visibility.is_edge_visible(i),
776                    kind: EntityKind::Edge(e.name.clone()),
777                })
778                .collect();
779            let vertices = s
780                .vertices
781                .iter()
782                .enumerate()
783                .map(|(i, v)| Entity {
784                    label: format!("Vertex {i}"),
785                    selected: sel_vertices.iter().any(|r| {
786                        r.solid == s.name
787                            && (r.position[0] - v.position[0]).abs() <= TOL
788                            && (r.position[1] - v.position[1]).abs() <= TOL
789                            && (r.position[2] - v.position[2]).abs() <= TOL
790                    }),
791                    visible: s.visibility.is_vertex_visible(i),
792                    kind: EntityKind::Vertex(v.position),
793                })
794                .collect();
795            SolidRow {
796                name: s.name.clone(),
797                visible: s.visible,
798                selected: sel_solids.contains(&s.name),
799                faces,
800                edges,
801                vertices,
802            }
803        })
804        .collect()
805}
806
807// BREP private tests: be80f6b7de9698c9
808
809/// The hit keys this panel publishes (see `automation::hit_keys`).
810pub static HIT_KEYS: &[HitKeyDoc] = &[
811    HitKeyDoc { panel: "scene", prefix: "box:", meaning: "expand/collapse a tree node (box:name, box:__scene, box:__datums)", command: None },
812    HitKeyDoc { panel: "scene", prefix: "sel:", meaning: "select a tree row (sel:name, sel:name/ei, sel:sketch/id, sel:datum/name)", command: None },
813    HitKeyDoc { panel: "scene", prefix: "vis:", meaning: "toggle a row's visibility (vis:name, vis:name/ei, vis:__scene)", command: None },
814    HitKeyDoc { panel: "scene", prefix: "typevis:", meaning: "toggle visibility of a whole kind (typevis:label)", command: None },
815];