1use crate::panels::toolbar_button;
57use crate::panels::tree::{self, TreeRow};
58use brep_render::engine_state::EngineState;
59use brep_render::visibility::{EntityKind as VisKind, GroupState};
60use eframe::egui;
61use std::collections::{HashMap, HashSet};
62
63#[derive(Clone)]
65enum EntityKind {
66 Face(String),
68 Edge(String),
70 Vertex([f64; 3]),
72}
73
74#[derive(Clone)]
78struct Entity {
79 label: String,
80 kind: EntityKind,
81 selected: bool,
82 visible: bool,
83}
84
85struct SolidRow {
88 name: String,
89 visible: bool,
90 selected: bool,
91 faces: Vec<Entity>,
92 edges: Vec<Entity>,
93 vertices: Vec<Entity>,
94}
95
96enum Action {
99 SetVisible(String, bool),
100 SetAllVisible(bool),
101 SetEntityVisible(String, VisKind, usize, bool),
103 SetGroupVisible(String, VisKind, bool),
105 SetAllGroupVisible(VisKind, bool),
107 SetAllSketchVisible(bool),
109 SetAllDatumVisible(bool),
111 Select(&'static str, String),
112 SelectVertex(String, [f64; 3]),
113 SetSketchVisible(String, bool),
115 SetDatumVisible(String, bool),
117 SelectDatum(String),
119}
120
121#[derive(Default)]
124pub struct ScenePanel {
125 hits: HashMap<String, egui::Rect>,
128 root_collapsed: bool,
130 collapsed_solids: HashSet<String>,
133 expanded_groups: HashSet<String>,
136 datums_collapsed: bool,
139}
140
141impl ScenePanel {
142 pub fn new() -> Self {
143 Self::default()
144 }
145
146 pub fn show(&mut self, ui: &mut egui::Ui, state: &mut EngineState) {
150 self.hits.clear();
151
152 let sel_solids = state.emphasis.selected_solids.clone();
154 let sel_faces = state.emphasis.selected_faces.clone();
155 let sel_edges = state.emphasis.selected_edges.clone();
156 let sel_vertices = state.emphasis.selected_vertices.clone();
157 let solids = snapshot(state, &sel_solids, &sel_faces, &sel_edges, &sel_vertices);
158 let sketches = state.committed_sketches();
160 let sel_datums = state.emphasis.selected_datums.clone();
163 let datums = state.construction_datums();
164
165 ui.spacing_mut().item_spacing.y = 2.0;
167
168 let mut action: Option<Action> = None;
169
170 ui.horizontal(|ui| {
178 let has_solids = !solids.is_empty();
179
180 let groups: [(VisKind, &str, bool); 3] = [
183 (
184 VisKind::Face,
185 "Faces",
186 has_solids
187 && solids
188 .iter()
189 .all(|s| matches!(group_state(&s.faces), GroupState::All)),
190 ),
191 (
192 VisKind::Edge,
193 "Edges",
194 has_solids
195 && solids
196 .iter()
197 .all(|s| matches!(group_state(&s.edges), GroupState::All)),
198 ),
199 (
200 VisKind::Vertex,
201 "Vertices",
202 has_solids
203 && solids
204 .iter()
205 .all(|s| matches!(group_state(&s.vertices), GroupState::All)),
206 ),
207 ];
208 for (kind, label, all_visible) in groups {
209 let tip = if all_visible {
210 format!("Hide all {}", label.to_lowercase())
211 } else {
212 format!("Show all {}", label.to_lowercase())
213 };
214 let resp = ui
217 .add_enabled_ui(has_solids, |ui| {
218 toolbar_button::toggle(ui, all_visible, label, &tip)
219 })
220 .inner;
221 self.hits.insert(format!("typevis:{label}"), resp.rect);
222 if resp.clicked() {
223 action = Some(Action::SetAllGroupVisible(kind, !all_visible));
224 }
225 }
226
227 if !sketches.is_empty() {
230 let all_visible = sketches.iter().all(|(_, v)| *v);
231 let tip = if all_visible {
232 "Hide all sketches"
233 } else {
234 "Show all sketches"
235 };
236 let resp = toolbar_button::toggle(ui, all_visible, "Sketches", tip);
237 self.hits.insert("typevis:Sketches".into(), resp.rect);
238 if resp.clicked() {
239 action = Some(Action::SetAllSketchVisible(!all_visible));
240 }
241 }
242
243 if !datums.is_empty() {
247 let all_visible = datums.iter().all(|(_, v)| *v);
248 let tip = if all_visible {
249 "Hide all planes & datums"
250 } else {
251 "Show all planes & datums"
252 };
253 let resp = toolbar_button::toggle(ui, all_visible, "Planes", tip);
254 self.hits.insert("typevis:Datums".into(), resp.rect);
255 if resp.clicked() {
256 action = Some(Action::SetAllDatumVisible(!all_visible));
257 }
258 }
259 });
260
261 let root_open = !self.root_collapsed;
263 let all_visible = !solids.is_empty() && solids.iter().all(|s| s.visible);
264 let mut root_vis = all_visible;
265 let mut root_vis_rect = egui::Rect::NOTHING;
266 let mut root_vis_clicked = false;
267 let root_resp = tree::node(
268 ui,
269 TreeRow {
270 guides: &[],
271 is_last: true,
272 expandable: true,
273 expanded: root_open,
274 root: true,
275 glyph: None,
276 label: "Scene",
277 selected: false,
278 draggable: false,
279 },
280 |ui| {
281 let cb = ui.add_enabled(!solids.is_empty(), egui::Checkbox::new(&mut root_vis, ""));
282 root_vis_rect = cb.rect;
283 root_vis_clicked = cb.clicked();
284 },
285 );
286 self.hits.insert("box:__scene".into(), root_resp.box_rect);
287 self.hits.insert("vis:__scene".into(), root_vis_rect);
288 if root_vis_clicked {
289 action = Some(Action::SetAllVisible(root_vis));
290 }
291 if root_resp.toggled || root_resp.label.clicked() {
292 self.root_collapsed = !self.root_collapsed;
293 }
294
295 if solids.is_empty() && sketches.is_empty() && datums.is_empty() {
296 let g = tree::child_guides(&[], true);
297 tree::node(ui, TreeRow::leaf(&g, true, "(scene is empty)"), |_| {});
298 }
299
300 if root_open {
301 let has_sketches = !sketches.is_empty();
307 let has_datums = !datums.is_empty();
308 let n = solids.len();
309 for (si, solid) in solids.iter().enumerate() {
310 let is_last = !has_sketches && !has_datums && si + 1 == n;
311 self.render_solid(ui, solid, is_last, &mut action);
312 }
313 let m = sketches.len();
314 for (i, (id, visible)) in sketches.iter().enumerate() {
315 let is_last = !has_datums && i + 1 == m;
316 let selected = sel_solids.contains(id);
317 self.render_sketch_row(ui, id, *visible, selected, is_last, &mut action);
318 }
319 if has_datums {
320 self.render_datums(ui, &datums, &sel_datums, &mut action);
321 }
322 }
323
324 match action {
326 Some(Action::SetVisible(name, v)) => {
327 state.set_visible(&name, v);
328 }
329 Some(Action::SetAllVisible(v)) => {
330 for s in &solids {
331 state.set_visible(&s.name, v);
332 }
333 }
334 Some(Action::SetEntityVisible(name, kind, index, v)) => {
335 state.set_entity_visible(&name, kind, index, v);
336 }
337 Some(Action::SetGroupVisible(name, kind, v)) => {
338 state.set_group_visible(&name, kind, v);
339 }
340 Some(Action::SetAllGroupVisible(kind, v)) => {
341 for s in &solids {
342 state.set_group_visible(&s.name, kind, v);
343 }
344 }
345 Some(Action::SetAllSketchVisible(v)) => {
346 for (id, _) in &sketches {
347 state.set_sketch_visible(id, v);
348 }
349 }
350 Some(Action::SetAllDatumVisible(v)) => {
351 for (name, _) in &datums {
352 state.set_datum_visible(name, v);
353 }
354 }
355 Some(Action::Select(kind, name)) => {
356 state.select_by_name(kind, &name);
357 }
358 Some(Action::SelectVertex(solid, pos)) => {
359 state.select_vertex_by_position(&solid, pos);
360 }
361 Some(Action::SetSketchVisible(id, v)) => {
362 state.set_sketch_visible(&id, v);
363 }
364 Some(Action::SetDatumVisible(name, v)) => {
365 state.set_datum_visible(&name, v);
366 }
367 Some(Action::SelectDatum(name)) => {
368 state.select_datum(&name);
369 }
370 None => {}
371 }
372
373 #[cfg(target_arch = "wasm32")]
378 {
379 publish("__brepScene", &state.scene_entities_json());
380 publish("__brepSketches", &state.sketch_entities_json());
381 publish("__brepDatums", &state.datum_entities_json());
382 publish("__brepSceneVis", &state.scene_visibility_json());
383 publish("__brepSceneHit", &self.hits_json());
384 }
385 }
386
387 fn render_sketch_row(
396 &mut self,
397 ui: &mut egui::Ui,
398 id: &str,
399 visible: bool,
400 selected: bool,
401 is_last: bool,
402 action: &mut Option<Action>,
403 ) {
404 let mut vis = visible;
405 let mut vis_rect = egui::Rect::NOTHING;
406 let mut vis_clicked = false;
407 let resp = tree::node(
408 ui,
409 TreeRow::leaf(&[], is_last, id).selected(selected),
410 |ui| {
411 let cb = ui.add(egui::Checkbox::new(&mut vis, ""));
412 vis_rect = cb.rect;
413 vis_clicked = cb.clicked();
414 },
415 );
416 self.hits.insert(format!("vis:sketch/{id}"), vis_rect);
417 self.hits.insert(format!("sel:sketch/{id}"), resp.label.rect);
418 if vis_clicked {
419 *action = Some(Action::SetSketchVisible(id.to_string(), vis));
420 } else if resp.label.clicked() {
421 *action = Some(Action::Select("solid", id.to_string()));
423 }
424 }
425
426 fn render_datums(
434 &mut self,
435 ui: &mut egui::Ui,
436 datums: &[(String, bool)],
437 sel_datums: &HashSet<String>,
438 action: &mut Option<Action>,
439 ) {
440 let open = !self.datums_collapsed;
441 let resp = tree::node(
442 ui,
443 TreeRow::branch(&[], true, open, "Planes & Datums"),
444 |ui| {
445 ui.add_space(6.0);
446 ui.label(egui::RichText::new(format!("{}", datums.len())).weak());
447 },
448 );
449 self.hits.insert("box:__datums".into(), resp.box_rect);
450 if resp.toggled || resp.label.clicked() {
451 self.datums_collapsed = !self.datums_collapsed;
452 }
453 if !open {
454 return;
455 }
456
457 let base = tree::child_guides(&[], true);
458 let m = datums.len();
459 for (i, (name, visible)) in datums.iter().enumerate() {
460 let last = i + 1 == m;
461 let selected = sel_datums.contains(name);
462 let mut vis = *visible;
463 let mut vis_rect = egui::Rect::NOTHING;
464 let mut vis_clicked = false;
465 let resp = tree::node(
466 ui,
467 TreeRow::leaf(&base, last, name).selected(selected),
468 |ui| {
469 let cb = ui.add(egui::Checkbox::new(&mut vis, ""));
470 vis_rect = cb.rect;
471 vis_clicked = cb.clicked();
472 },
473 );
474 self.hits.insert(format!("vis:datum/{name}"), vis_rect);
475 self.hits.insert(format!("sel:datum/{name}"), resp.label.rect);
476 if vis_clicked {
477 *action = Some(Action::SetDatumVisible(name.clone(), vis));
478 } else if resp.label.clicked() {
479 *action = Some(Action::SelectDatum(name.clone()));
480 }
481 }
482 }
483
484 fn render_solid(
486 &mut self,
487 ui: &mut egui::Ui,
488 solid: &SolidRow,
489 is_last: bool,
490 action: &mut Option<Action>,
491 ) {
492 let name = &solid.name;
493 let open = !self.collapsed_solids.contains(name);
494
495 let mut vis = solid.visible;
496 let mut vis_rect = egui::Rect::NOTHING;
497 let mut vis_clicked = false;
498 let resp = tree::node(
499 ui,
500 TreeRow::branch(&[], is_last, open, name).selected(solid.selected),
501 |ui| {
502 let cb = ui.add(egui::Checkbox::new(&mut vis, ""));
503 vis_rect = cb.rect;
504 vis_clicked = cb.clicked();
505 },
506 );
507 self.hits.insert(format!("box:{name}"), resp.box_rect);
508 self.hits.insert(format!("vis:{name}"), vis_rect);
509 self.hits.insert(format!("sel:{name}"), resp.label.rect);
510
511 if vis_clicked {
512 *action = Some(Action::SetVisible(name.clone(), vis));
513 }
514 if resp.toggled {
515 if open {
516 self.collapsed_solids.insert(name.clone());
517 } else {
518 self.collapsed_solids.remove(name);
519 }
520 }
521 if resp.label.clicked() {
522 *action = Some(Action::Select("solid", name.clone()));
523 }
524
525 if open {
526 let base = tree::child_guides(&[], is_last);
527 self.render_group(ui, name, &base, false, "Faces", VisKind::Face, &solid.faces, action);
528 self.render_group(ui, name, &base, false, "Edges", VisKind::Edge, &solid.edges, action);
529 self.render_group(ui, name, &base, true, "Vertices", VisKind::Vertex, &solid.vertices, action);
530 }
531 }
532
533 #[allow(clippy::too_many_arguments)]
537 fn render_group(
538 &mut self,
539 ui: &mut egui::Ui,
540 solid_name: &str,
541 base: &[bool],
542 is_last: bool,
543 group: &str,
544 kind: VisKind,
545 entities: &[Entity],
546 action: &mut Option<Action>,
547 ) {
548 let key = format!("{solid_name}/{group}");
549 let open = self.expanded_groups.contains(&key);
550
551 let state = group_state(entities);
553 let mut checked = matches!(state, GroupState::All);
554 let indeterminate = matches!(state, GroupState::Partial);
555 let mut vis_rect = egui::Rect::NOTHING;
556 let mut vis_clicked = false;
557 let resp = tree::node(
558 ui,
559 TreeRow::branch(base, is_last, open, group),
560 |ui| {
561 let cb = ui.add(
563 egui::Checkbox::new(&mut checked, "").indeterminate(indeterminate),
564 );
565 vis_rect = cb.rect;
566 vis_clicked = cb.clicked();
567 ui.add_space(6.0);
568 ui.label(egui::RichText::new(format!("{}", entities.len())).weak());
569 },
570 );
571 self.hits.insert(format!("box:{key}"), resp.box_rect);
572 self.hits.insert(format!("vis:{key}"), vis_rect);
573 if vis_clicked {
574 let want_visible = !matches!(state, GroupState::All);
576 *action = Some(Action::SetGroupVisible(solid_name.to_string(), kind, want_visible));
577 }
578 if resp.toggled || resp.label.clicked() {
579 if open {
580 self.expanded_groups.remove(&key);
581 } else {
582 self.expanded_groups.insert(key.clone());
583 }
584 }
585
586 if !open {
587 return;
588 }
589 let gg = tree::child_guides(base, is_last);
590 if entities.is_empty() {
591 tree::node(ui, TreeRow::leaf(&gg, true, "(none)"), |_| {});
592 return;
593 }
594 let m = entities.len();
595 for (ei, e) in entities.iter().enumerate() {
596 let last = ei + 1 == m;
597 let mut vis = e.visible;
598 let mut ev_rect = egui::Rect::NOTHING;
599 let mut ev_clicked = false;
600 let resp = tree::node(
601 ui,
602 TreeRow::leaf(&gg, last, &e.label).selected(e.selected),
603 |ui| {
604 let cb = ui.add(egui::Checkbox::new(&mut vis, ""));
605 ev_rect = cb.rect;
606 ev_clicked = cb.clicked();
607 },
608 );
609 self.hits.insert(format!("sel:{key}/{ei}"), resp.label.rect);
610 self.hits.insert(format!("vis:{key}/{ei}"), ev_rect);
611 if ev_clicked {
612 *action = Some(Action::SetEntityVisible(solid_name.to_string(), kind, ei, vis));
613 } else if resp.label.clicked() {
614 *action = Some(match &e.kind {
615 EntityKind::Face(n) => Action::Select("face", n.clone()),
616 EntityKind::Edge(n) => Action::Select("edge", n.clone()),
617 EntityKind::Vertex(p) => Action::SelectVertex(solid_name.to_string(), *p),
618 });
619 }
620 }
621 }
622
623 #[cfg(target_arch = "wasm32")]
625 pub fn hits_json(&self) -> String {
626 let map: serde_json::Map<String, serde_json::Value> = self
627 .hits
628 .iter()
629 .map(|(k, r)| {
630 (
631 k.clone(),
632 serde_json::json!([r.min.x, r.min.y, r.width(), r.height()]),
633 )
634 })
635 .collect();
636 serde_json::Value::Object(map).to_string()
637 }
638}
639
640fn group_state(entities: &[Entity]) -> GroupState {
644 if entities.is_empty() {
645 return GroupState::All;
646 }
647 let visible = entities.iter().filter(|e| e.visible).count();
648 if visible == entities.len() {
649 GroupState::All
650 } else if visible == 0 {
651 GroupState::None
652 } else {
653 GroupState::Partial
654 }
655}
656
657fn snapshot(
663 state: &EngineState,
664 sel_solids: &HashSet<String>,
665 sel_faces: &HashSet<String>,
666 sel_edges: &HashSet<String>,
667 sel_vertices: &[brep_render::style::VertexRef],
668) -> Vec<SolidRow> {
669 const TOL: f64 = 1e-6;
672 state
673 .scene
674 .solids()
675 .iter()
676 .filter(|s| !s.is_sketch)
681 .map(|s| {
682 let faces = s
683 .faces
684 .iter()
685 .enumerate()
686 .map(|(i, f)| Entity {
687 label: if f.name.is_empty() {
688 format!("Face {i}")
689 } else {
690 f.name.clone()
691 },
692 selected: !f.name.is_empty() && sel_faces.contains(&f.name),
693 visible: s.visibility.is_face_visible(i),
694 kind: EntityKind::Face(f.name.clone()),
695 })
696 .collect();
697 let edges = s
698 .edges
699 .iter()
700 .enumerate()
701 .map(|(i, e)| Entity {
702 label: if e.name.is_empty() {
703 format!("Edge {i}")
704 } else {
705 e.name.clone()
706 },
707 selected: !e.name.is_empty() && sel_edges.contains(&e.name),
708 visible: s.visibility.is_edge_visible(i),
709 kind: EntityKind::Edge(e.name.clone()),
710 })
711 .collect();
712 let vertices = s
713 .vertices
714 .iter()
715 .enumerate()
716 .map(|(i, v)| Entity {
717 label: format!("Vertex {i}"),
718 selected: sel_vertices.iter().any(|r| {
719 r.solid == s.name
720 && (r.position[0] - v.position[0]).abs() <= TOL
721 && (r.position[1] - v.position[1]).abs() <= TOL
722 && (r.position[2] - v.position[2]).abs() <= TOL
723 }),
724 visible: s.visibility.is_vertex_visible(i),
725 kind: EntityKind::Vertex(v.position),
726 })
727 .collect();
728 SolidRow {
729 name: s.name.clone(),
730 visible: s.visible,
731 selected: sel_solids.contains(&s.name),
732 faces,
733 edges,
734 vertices,
735 }
736 })
737 .collect()
738}
739
740#[cfg(target_arch = "wasm32")]
742fn publish(name: &str, json: &str) {
743 if let Some(win) = web_sys::window() {
744 let _ = js_sys::Reflect::set(
745 &win,
746 &wasm_bindgen::JsValue::from_str(name),
747 &wasm_bindgen::JsValue::from_str(json),
748 );
749 }
750}
751
752#[cfg(test)]
753mod tests {
754 use super::*;
755
756 fn cube_history(name: &str) -> String {
760 serde_json::json!({
761 "expressions": "",
762 "configurator": {},
763 "features": [{
764 "type": "P.CU",
765 "inputParams": {
766 "id": name,
767 "sizeX": 10.0, "sizeY": 10.0, "sizeZ": 10.0,
768 "transform": {
769 "position": [0.0, 0.0, 0.0],
770 "rotationEuler": [0.0, 0.0, 0.0],
771 "scale": [1.0, 1.0, 1.0]
772 },
773 "boolean": { "targets": [], "operation": "NONE" }
774 },
775 "persistentData": {}
776 }]
777 })
778 .to_string()
779 }
780
781 fn run_frame(
786 ctx: &egui::Context,
787 panel: &mut ScenePanel,
788 state: &mut EngineState,
789 events: Vec<egui::Event>,
790 scroll: bool,
791 ) {
792 let raw = egui::RawInput {
793 screen_rect: Some(egui::Rect::from_min_size(
794 egui::pos2(0.0, 0.0),
795 egui::vec2(800.0, 600.0),
796 )),
797 events,
798 ..Default::default()
799 };
800 let _ = ctx.run_ui(raw, |ui| {
801 if scroll {
806 egui::ScrollArea::vertical().show(ui, |ui| panel.show(ui, state));
807 } else {
808 panel.show(ui, state);
809 }
810 });
811 }
812
813 fn click_at(
817 ctx: &egui::Context,
818 panel: &mut ScenePanel,
819 state: &mut EngineState,
820 pos: egui::Pos2,
821 scroll: bool,
822 ) {
823 run_frame(
824 ctx,
825 panel,
826 state,
827 vec![
828 egui::Event::PointerMoved(pos),
829 egui::Event::PointerButton {
830 pos,
831 button: egui::PointerButton::Primary,
832 pressed: true,
833 modifiers: egui::Modifiers::default(),
834 },
835 ],
836 scroll,
837 );
838 run_frame(
839 ctx,
840 panel,
841 state,
842 vec![egui::Event::PointerButton {
843 pos,
844 button: egui::PointerButton::Primary,
845 pressed: false,
846 modifiers: egui::Modifiers::default(),
847 }],
848 scroll,
849 );
850 }
851
852 fn drive_group_checkbox(kind: VisKind, hit_key: &str, scroll: bool) {
859 let ctx = egui::Context::default();
860 let mut state = EngineState::new();
861 state.set_history_json(&cube_history("Box")).unwrap();
862 let mut panel = ScenePanel::new();
863
864 run_frame(&ctx, &mut panel, &mut state, vec![], scroll);
866 assert_eq!(
867 state.group_visibility("Box", kind),
868 Some(GroupState::All),
869 "precondition: every {kind:?} starts visible"
870 );
871 let rect = *panel.hits.get(hit_key).unwrap_or_else(|| {
872 panic!(
873 "missing hit rect {hit_key}; have {:?}",
874 panel.hits.keys().collect::<Vec<_>>()
875 )
876 });
877
878 click_at(&ctx, &mut panel, &mut state, rect.center(), scroll);
880 assert_eq!(
881 state.group_visibility("Box", kind),
882 Some(GroupState::None),
883 "clicking the {kind:?} group checkbox must hide the whole group"
884 );
885
886 run_frame(&ctx, &mut panel, &mut state, vec![], scroll);
888 let rect = *panel.hits.get(hit_key).expect("checkbox rect after hide");
889 click_at(&ctx, &mut panel, &mut state, rect.center(), scroll);
890 assert_eq!(
891 state.group_visibility("Box", kind),
892 Some(GroupState::All),
893 "re-clicking the {kind:?} group checkbox must re-show the whole group"
894 );
895 }
896
897 #[test]
898 fn scene_tree_faces_group_checkbox_hides_and_shows_through_egui() {
899 drive_group_checkbox(VisKind::Face, "vis:Box/Faces", false);
900 }
901
902 #[test]
903 fn scene_tree_edges_group_checkbox_hides_and_shows_through_egui() {
904 drive_group_checkbox(VisKind::Edge, "vis:Box/Edges", false);
905 }
906
907 #[test]
911 fn scene_tree_faces_group_checkbox_works_inside_scroll_area() {
912 drive_group_checkbox(VisKind::Face, "vis:Box/Faces", true);
913 }
914
915 #[test]
920 fn scene_tree_individual_face_checkbox_hides_one_face_through_egui() {
921 let ctx = egui::Context::default();
922 let mut state = EngineState::new();
923 state.set_history_json(&cube_history("Box")).unwrap();
924 let mut panel = ScenePanel::new();
925
926 run_frame(&ctx, &mut panel, &mut state, vec![], false);
928 let box_rect = *panel.hits.get("box:Box/Faces").expect("faces group collapse box");
929
930 click_at(&ctx, &mut panel, &mut state, box_rect.center(), false);
932 run_frame(&ctx, &mut panel, &mut state, vec![], false);
933 assert_eq!(
934 state.entity_visible("Box", VisKind::Face, 0),
935 Some(true),
936 "face 0 starts visible"
937 );
938 let leaf = *panel.hits.get("vis:Box/Faces/0").unwrap_or_else(|| {
939 panic!(
940 "missing face-0 leaf checkbox; have {:?}",
941 panel.hits.keys().collect::<Vec<_>>()
942 )
943 });
944
945 click_at(&ctx, &mut panel, &mut state, leaf.center(), false);
947 assert_eq!(
948 state.entity_visible("Box", VisKind::Face, 0),
949 Some(false),
950 "clicking face 0's checkbox must hide exactly face 0"
951 );
952 assert_eq!(
953 state.entity_visible("Box", VisKind::Face, 1),
954 Some(true),
955 "sibling face 1 stays visible"
956 );
957 assert_eq!(
958 state.group_visibility("Box", VisKind::Face),
959 Some(GroupState::Partial),
960 "one hidden face → group reads Partial"
961 );
962 }
963
964 fn two_cube_history() -> String {
969 let cube = |name: &str, x: f64| {
970 serde_json::json!({
971 "type": "P.CU",
972 "inputParams": {
973 "id": name,
974 "sizeX": 10.0, "sizeY": 10.0, "sizeZ": 10.0,
975 "transform": {
976 "position": [x, 0.0, 0.0],
977 "rotationEuler": [0.0, 0.0, 0.0],
978 "scale": [1.0, 1.0, 1.0]
979 },
980 "boolean": { "targets": [], "operation": "NONE" }
981 },
982 "persistentData": {}
983 })
984 };
985 serde_json::json!({
986 "expressions": "",
987 "configurator": {},
988 "features": [cube("Box1", 0.0), cube("Box2", 20.0)]
989 })
990 .to_string()
991 }
992
993 fn drive_type_button(kind: VisKind, hit_key: &str) {
1000 let ctx = egui::Context::default();
1001 let mut state = EngineState::new();
1002 state.set_history_json(&two_cube_history()).unwrap();
1003 let mut panel = ScenePanel::new();
1004
1005 run_frame(&ctx, &mut panel, &mut state, vec![], false);
1007 for name in ["Box1", "Box2"] {
1008 assert_eq!(
1009 state.group_visibility(name, kind),
1010 Some(GroupState::All),
1011 "precondition: every {kind:?} on {name} starts visible"
1012 );
1013 }
1014 let rect = *panel.hits.get(hit_key).unwrap_or_else(|| {
1015 panic!(
1016 "missing hit rect {hit_key}; have {:?}",
1017 panel.hits.keys().collect::<Vec<_>>()
1018 )
1019 });
1020
1021 click_at(&ctx, &mut panel, &mut state, rect.center(), false);
1023 for name in ["Box1", "Box2"] {
1024 assert_eq!(
1025 state.group_visibility(name, kind),
1026 Some(GroupState::None),
1027 "the scene-wide {kind:?} button must hide {name}'s whole group"
1028 );
1029 }
1030
1031 run_frame(&ctx, &mut panel, &mut state, vec![], false);
1033 let rect = *panel.hits.get(hit_key).expect("button rect after hide");
1034 click_at(&ctx, &mut panel, &mut state, rect.center(), false);
1035 for name in ["Box1", "Box2"] {
1036 assert_eq!(
1037 state.group_visibility(name, kind),
1038 Some(GroupState::All),
1039 "re-clicking the scene-wide {kind:?} button must re-show {name}'s group"
1040 );
1041 }
1042 }
1043
1044 #[test]
1045 fn scene_type_faces_button_hides_and_shows_all_solids_through_egui() {
1046 drive_type_button(VisKind::Face, "typevis:Faces");
1047 }
1048
1049 #[test]
1050 fn scene_type_edges_button_hides_and_shows_all_solids_through_egui() {
1051 drive_type_button(VisKind::Edge, "typevis:Edges");
1052 }
1053
1054 #[test]
1057 fn scene_type_vertices_button_hides_and_shows_all_solids_through_egui() {
1058 drive_type_button(VisKind::Vertex, "typevis:Vertices");
1059 }
1060
1061 fn cube_and_sketch_history() -> String {
1065 serde_json::json!({
1066 "expressions": "",
1067 "configurator": {},
1068 "features": [
1069 {
1070 "type": "P.CU",
1071 "inputParams": {
1072 "id": "Box",
1073 "sizeX": 10.0, "sizeY": 10.0, "sizeZ": 10.0,
1074 "transform": {
1075 "position": [0.0, 0.0, 0.0],
1076 "rotationEuler": [0.0, 0.0, 0.0],
1077 "scale": [1.0, 1.0, 1.0]
1078 },
1079 "boolean": { "targets": [], "operation": "NONE" }
1080 },
1081 "persistentData": {}
1082 },
1083 {
1084 "type": "S",
1085 "inputParams": { "id": "Sk" },
1086 "persistentData": {
1087 "basis": { "origin": [0, 0, 0], "x": [1, 0, 0], "y": [0, 1, 0], "z": [0, 0, 1] },
1088 "sketch": {
1089 "points": [
1090 { "id": 0, "x": 0.0, "y": 0.0 },
1091 { "id": 1, "x": 10.0, "y": 0.0 },
1092 { "id": 2, "x": 10.0, "y": 6.0 },
1093 { "id": 3, "x": 0.0, "y": 6.0 }
1094 ],
1095 "geometries": [
1096 { "id": 10, "type": "line", "points": [0, 1] },
1097 { "id": 11, "type": "line", "points": [1, 2] },
1098 { "id": 12, "type": "line", "points": [2, 3] },
1099 { "id": 13, "type": "line", "points": [3, 0] }
1100 ],
1101 "constraints": []
1102 }
1103 }
1104 }
1105 ]
1106 })
1107 .to_string()
1108 }
1109
1110 #[test]
1117 fn committed_sketch_is_a_top_level_row_not_a_group() {
1118 let ctx = egui::Context::default();
1119 let mut state = EngineState::new();
1120 state.set_history_json(&cube_and_sketch_history()).unwrap();
1121 let mut panel = ScenePanel::new();
1122
1123 run_frame(&ctx, &mut panel, &mut state, vec![], false);
1125 let keys = || panel.hits.keys().cloned().collect::<Vec<_>>();
1126
1127 assert!(
1129 panel.hits.contains_key("sel:sketch/Sk"),
1130 "sketch select row present: {:?}",
1131 keys()
1132 );
1133 assert!(
1134 panel.hits.contains_key("vis:sketch/Sk"),
1135 "sketch visibility checkbox present: {:?}",
1136 keys()
1137 );
1138 assert!(
1140 !panel.hits.contains_key("box:__sketches"),
1141 "no Sketches group node: {:?}",
1142 keys()
1143 );
1144 assert!(
1146 !panel.hits.contains_key("box:Sk"),
1147 "sketch is not a plain solid branch: {:?}",
1148 keys()
1149 );
1150 assert!(
1151 !panel.hits.contains_key("vis:Sk"),
1152 "sketch is not a plain solid checkbox: {:?}",
1153 keys()
1154 );
1155 assert!(
1157 panel.hits.contains_key("box:Box"),
1158 "cube solid branch present: {:?}",
1159 keys()
1160 );
1161 }
1162}