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