1use crate::panels::tree::{self, TreeRow};
48use brep_render::engine_state::EngineState;
49use brep_render::visibility::{EntityKind as VisKind, GroupState};
50use eframe::egui;
51use std::collections::{HashMap, HashSet};
52
53#[derive(Clone)]
55enum EntityKind {
56 Face(String),
58 Edge(String),
60 Vertex([f64; 3]),
62}
63
64#[derive(Clone)]
68struct Entity {
69 label: String,
70 kind: EntityKind,
71 selected: bool,
72 visible: bool,
73}
74
75struct SolidRow {
78 name: String,
79 visible: bool,
80 selected: bool,
81 faces: Vec<Entity>,
82 edges: Vec<Entity>,
83 vertices: Vec<Entity>,
84}
85
86enum Action {
89 SetVisible(String, bool),
90 SetAllVisible(bool),
91 SetEntityVisible(String, VisKind, usize, bool),
93 SetGroupVisible(String, VisKind, bool),
95 Select(&'static str, String),
96 SelectVertex(String, [f64; 3]),
97 SetSketchVisible(String, bool),
99 SetDatumVisible(String, bool),
101 SelectDatum(String),
103}
104
105#[derive(Default)]
108pub struct ScenePanel {
109 hits: HashMap<String, egui::Rect>,
112 root_collapsed: bool,
114 collapsed_solids: HashSet<String>,
117 expanded_groups: HashSet<String>,
120 sketches_collapsed: bool,
123 datums_collapsed: bool,
126}
127
128impl ScenePanel {
129 pub fn new() -> Self {
130 Self::default()
131 }
132
133 pub fn show(&mut self, ui: &mut egui::Ui, state: &mut EngineState) {
137 self.hits.clear();
138
139 let sel_solids = state.emphasis.selected_solids.clone();
141 let sel_faces = state.emphasis.selected_faces.clone();
142 let sel_edges = state.emphasis.selected_edges.clone();
143 let sel_vertices = state.emphasis.selected_vertices.clone();
144 let solids = snapshot(state, &sel_solids, &sel_faces, &sel_edges, &sel_vertices);
145 let sketches = state.committed_sketches();
147 let sel_datums = state.emphasis.selected_datums.clone();
150 let datums = state.construction_datums();
151
152 ui.spacing_mut().item_spacing.y = 2.0;
154
155 let mut action: Option<Action> = None;
156
157 let root_open = !self.root_collapsed;
159 let all_visible = !solids.is_empty() && solids.iter().all(|s| s.visible);
160 let mut root_vis = all_visible;
161 let mut root_vis_rect = egui::Rect::NOTHING;
162 let mut root_vis_clicked = false;
163 let root_resp = tree::node(
164 ui,
165 TreeRow {
166 guides: &[],
167 is_last: true,
168 expandable: true,
169 expanded: root_open,
170 root: true,
171 glyph: None,
172 label: "Scene",
173 selected: false,
174 draggable: false,
175 },
176 |ui| {
177 let cb = ui.add_enabled(!solids.is_empty(), egui::Checkbox::new(&mut root_vis, ""));
178 root_vis_rect = cb.rect;
179 root_vis_clicked = cb.clicked();
180 },
181 );
182 self.hits.insert("box:__scene".into(), root_resp.box_rect);
183 self.hits.insert("vis:__scene".into(), root_vis_rect);
184 if root_vis_clicked {
185 action = Some(Action::SetAllVisible(root_vis));
186 }
187 if root_resp.toggled || root_resp.label.clicked() {
188 self.root_collapsed = !self.root_collapsed;
189 }
190
191 if solids.is_empty() && sketches.is_empty() && datums.is_empty() {
192 let g = tree::child_guides(&[], true);
193 tree::node(ui, TreeRow::leaf(&g, true, "(scene is empty)"), |_| {});
194 }
195
196 if root_open {
197 let has_sketches = !sketches.is_empty();
201 let has_datums = !datums.is_empty();
202 let n = solids.len();
203 for (si, solid) in solids.iter().enumerate() {
204 let is_last = !has_sketches && !has_datums && si + 1 == n;
205 self.render_solid(ui, solid, is_last, &mut action);
206 }
207 if has_sketches {
208 self.render_sketches(ui, &sketches, &sel_solids, !has_datums, &mut action);
209 }
210 if has_datums {
211 self.render_datums(ui, &datums, &sel_datums, &mut action);
212 }
213 }
214
215 match action {
217 Some(Action::SetVisible(name, v)) => {
218 state.set_visible(&name, v);
219 }
220 Some(Action::SetAllVisible(v)) => {
221 for s in &solids {
222 state.set_visible(&s.name, v);
223 }
224 }
225 Some(Action::SetEntityVisible(name, kind, index, v)) => {
226 state.set_entity_visible(&name, kind, index, v);
227 }
228 Some(Action::SetGroupVisible(name, kind, v)) => {
229 state.set_group_visible(&name, kind, v);
230 }
231 Some(Action::Select(kind, name)) => {
232 state.select_by_name(kind, &name);
233 }
234 Some(Action::SelectVertex(solid, pos)) => {
235 state.select_vertex_by_position(&solid, pos);
236 }
237 Some(Action::SetSketchVisible(id, v)) => {
238 state.set_sketch_visible(&id, v);
239 }
240 Some(Action::SetDatumVisible(name, v)) => {
241 state.set_datum_visible(&name, v);
242 }
243 Some(Action::SelectDatum(name)) => {
244 state.select_datum(&name);
245 }
246 None => {}
247 }
248
249 #[cfg(target_arch = "wasm32")]
254 {
255 publish("__brepScene", &state.scene_entities_json());
256 publish("__brepSketches", &state.sketch_entities_json());
257 publish("__brepDatums", &state.datum_entities_json());
258 publish("__brepSceneVis", &state.scene_visibility_json());
259 publish("__brepSceneHit", &self.hits_json());
260 }
261 }
262
263 fn render_sketches(
270 &mut self,
271 ui: &mut egui::Ui,
272 sketches: &[(String, bool)],
273 sel_solids: &HashSet<String>,
274 is_last: bool,
275 action: &mut Option<Action>,
276 ) {
277 let open = !self.sketches_collapsed;
278 let resp = tree::node(
279 ui,
280 TreeRow::branch(&[], is_last, open, "Sketches"),
281 |ui| {
282 ui.add_space(6.0);
283 ui.label(egui::RichText::new(format!("{}", sketches.len())).weak());
284 },
285 );
286 self.hits.insert("box:__sketches".into(), resp.box_rect);
287 if resp.toggled || resp.label.clicked() {
288 self.sketches_collapsed = !self.sketches_collapsed;
289 }
290 if !open {
291 return;
292 }
293
294 let base = tree::child_guides(&[], is_last);
295 let m = sketches.len();
296 for (i, (id, visible)) in sketches.iter().enumerate() {
297 let last = i + 1 == m;
298 let selected = sel_solids.contains(id);
299 let mut vis = *visible;
300 let mut vis_rect = egui::Rect::NOTHING;
301 let mut vis_clicked = false;
302 let resp = tree::node(
303 ui,
304 TreeRow::leaf(&base, last, id).selected(selected),
305 |ui| {
306 let cb = ui.add(egui::Checkbox::new(&mut vis, ""));
307 vis_rect = cb.rect;
308 vis_clicked = cb.clicked();
309 },
310 );
311 self.hits.insert(format!("vis:sketch/{id}"), vis_rect);
312 self.hits.insert(format!("sel:sketch/{id}"), resp.label.rect);
313 if vis_clicked {
314 *action = Some(Action::SetSketchVisible(id.clone(), vis));
315 } else if resp.label.clicked() {
316 *action = Some(Action::Select("solid", id.clone()));
318 }
319 }
320 }
321
322 fn render_datums(
330 &mut self,
331 ui: &mut egui::Ui,
332 datums: &[(String, bool)],
333 sel_datums: &HashSet<String>,
334 action: &mut Option<Action>,
335 ) {
336 let open = !self.datums_collapsed;
337 let resp = tree::node(
338 ui,
339 TreeRow::branch(&[], true, open, "Planes & Datums"),
340 |ui| {
341 ui.add_space(6.0);
342 ui.label(egui::RichText::new(format!("{}", datums.len())).weak());
343 },
344 );
345 self.hits.insert("box:__datums".into(), resp.box_rect);
346 if resp.toggled || resp.label.clicked() {
347 self.datums_collapsed = !self.datums_collapsed;
348 }
349 if !open {
350 return;
351 }
352
353 let base = tree::child_guides(&[], true);
354 let m = datums.len();
355 for (i, (name, visible)) in datums.iter().enumerate() {
356 let last = i + 1 == m;
357 let selected = sel_datums.contains(name);
358 let mut vis = *visible;
359 let mut vis_rect = egui::Rect::NOTHING;
360 let mut vis_clicked = false;
361 let resp = tree::node(
362 ui,
363 TreeRow::leaf(&base, last, name).selected(selected),
364 |ui| {
365 let cb = ui.add(egui::Checkbox::new(&mut vis, ""));
366 vis_rect = cb.rect;
367 vis_clicked = cb.clicked();
368 },
369 );
370 self.hits.insert(format!("vis:datum/{name}"), vis_rect);
371 self.hits.insert(format!("sel:datum/{name}"), resp.label.rect);
372 if vis_clicked {
373 *action = Some(Action::SetDatumVisible(name.clone(), vis));
374 } else if resp.label.clicked() {
375 *action = Some(Action::SelectDatum(name.clone()));
376 }
377 }
378 }
379
380 fn render_solid(
382 &mut self,
383 ui: &mut egui::Ui,
384 solid: &SolidRow,
385 is_last: bool,
386 action: &mut Option<Action>,
387 ) {
388 let name = &solid.name;
389 let open = !self.collapsed_solids.contains(name);
390
391 let mut vis = solid.visible;
392 let mut vis_rect = egui::Rect::NOTHING;
393 let mut vis_clicked = false;
394 let resp = tree::node(
395 ui,
396 TreeRow::branch(&[], is_last, open, name).selected(solid.selected),
397 |ui| {
398 let cb = ui.add(egui::Checkbox::new(&mut vis, ""));
399 vis_rect = cb.rect;
400 vis_clicked = cb.clicked();
401 },
402 );
403 self.hits.insert(format!("box:{name}"), resp.box_rect);
404 self.hits.insert(format!("vis:{name}"), vis_rect);
405 self.hits.insert(format!("sel:{name}"), resp.label.rect);
406
407 if vis_clicked {
408 *action = Some(Action::SetVisible(name.clone(), vis));
409 }
410 if resp.toggled {
411 if open {
412 self.collapsed_solids.insert(name.clone());
413 } else {
414 self.collapsed_solids.remove(name);
415 }
416 }
417 if resp.label.clicked() {
418 *action = Some(Action::Select("solid", name.clone()));
419 }
420
421 if open {
422 let base = tree::child_guides(&[], is_last);
423 self.render_group(ui, name, &base, false, "Faces", VisKind::Face, &solid.faces, action);
424 self.render_group(ui, name, &base, false, "Edges", VisKind::Edge, &solid.edges, action);
425 self.render_group(ui, name, &base, true, "Vertices", VisKind::Vertex, &solid.vertices, action);
426 }
427 }
428
429 #[allow(clippy::too_many_arguments)]
433 fn render_group(
434 &mut self,
435 ui: &mut egui::Ui,
436 solid_name: &str,
437 base: &[bool],
438 is_last: bool,
439 group: &str,
440 kind: VisKind,
441 entities: &[Entity],
442 action: &mut Option<Action>,
443 ) {
444 let key = format!("{solid_name}/{group}");
445 let open = self.expanded_groups.contains(&key);
446
447 let state = group_state(entities);
449 let mut checked = matches!(state, GroupState::All);
450 let indeterminate = matches!(state, GroupState::Partial);
451 let mut vis_rect = egui::Rect::NOTHING;
452 let mut vis_clicked = false;
453 let resp = tree::node(
454 ui,
455 TreeRow::branch(base, is_last, open, group),
456 |ui| {
457 let cb = ui.add(
459 egui::Checkbox::new(&mut checked, "").indeterminate(indeterminate),
460 );
461 vis_rect = cb.rect;
462 vis_clicked = cb.clicked();
463 ui.add_space(6.0);
464 ui.label(egui::RichText::new(format!("{}", entities.len())).weak());
465 },
466 );
467 self.hits.insert(format!("box:{key}"), resp.box_rect);
468 self.hits.insert(format!("vis:{key}"), vis_rect);
469 if vis_clicked {
470 let want_visible = !matches!(state, GroupState::All);
472 *action = Some(Action::SetGroupVisible(solid_name.to_string(), kind, want_visible));
473 }
474 if resp.toggled || resp.label.clicked() {
475 if open {
476 self.expanded_groups.remove(&key);
477 } else {
478 self.expanded_groups.insert(key.clone());
479 }
480 }
481
482 if !open {
483 return;
484 }
485 let gg = tree::child_guides(base, is_last);
486 if entities.is_empty() {
487 tree::node(ui, TreeRow::leaf(&gg, true, "(none)"), |_| {});
488 return;
489 }
490 let m = entities.len();
491 for (ei, e) in entities.iter().enumerate() {
492 let last = ei + 1 == m;
493 let mut vis = e.visible;
494 let mut ev_rect = egui::Rect::NOTHING;
495 let mut ev_clicked = false;
496 let resp = tree::node(
497 ui,
498 TreeRow::leaf(&gg, last, &e.label).selected(e.selected),
499 |ui| {
500 let cb = ui.add(egui::Checkbox::new(&mut vis, ""));
501 ev_rect = cb.rect;
502 ev_clicked = cb.clicked();
503 },
504 );
505 self.hits.insert(format!("sel:{key}/{ei}"), resp.label.rect);
506 self.hits.insert(format!("vis:{key}/{ei}"), ev_rect);
507 if ev_clicked {
508 *action = Some(Action::SetEntityVisible(solid_name.to_string(), kind, ei, vis));
509 } else if resp.label.clicked() {
510 *action = Some(match &e.kind {
511 EntityKind::Face(n) => Action::Select("face", n.clone()),
512 EntityKind::Edge(n) => Action::Select("edge", n.clone()),
513 EntityKind::Vertex(p) => Action::SelectVertex(solid_name.to_string(), *p),
514 });
515 }
516 }
517 }
518
519 #[cfg(target_arch = "wasm32")]
521 pub fn hits_json(&self) -> String {
522 let map: serde_json::Map<String, serde_json::Value> = self
523 .hits
524 .iter()
525 .map(|(k, r)| {
526 (
527 k.clone(),
528 serde_json::json!([r.min.x, r.min.y, r.width(), r.height()]),
529 )
530 })
531 .collect();
532 serde_json::Value::Object(map).to_string()
533 }
534}
535
536fn group_state(entities: &[Entity]) -> GroupState {
540 if entities.is_empty() {
541 return GroupState::All;
542 }
543 let visible = entities.iter().filter(|e| e.visible).count();
544 if visible == entities.len() {
545 GroupState::All
546 } else if visible == 0 {
547 GroupState::None
548 } else {
549 GroupState::Partial
550 }
551}
552
553fn snapshot(
559 state: &EngineState,
560 sel_solids: &HashSet<String>,
561 sel_faces: &HashSet<String>,
562 sel_edges: &HashSet<String>,
563 sel_vertices: &[brep_render::style::VertexRef],
564) -> Vec<SolidRow> {
565 const TOL: f64 = 1e-6;
568 state
569 .scene
570 .solids()
571 .iter()
572 .map(|s| {
573 let faces = s
574 .faces
575 .iter()
576 .enumerate()
577 .map(|(i, f)| Entity {
578 label: if f.name.is_empty() {
579 format!("Face {i}")
580 } else {
581 f.name.clone()
582 },
583 selected: !f.name.is_empty() && sel_faces.contains(&f.name),
584 visible: s.visibility.is_face_visible(i),
585 kind: EntityKind::Face(f.name.clone()),
586 })
587 .collect();
588 let edges = s
589 .edges
590 .iter()
591 .enumerate()
592 .map(|(i, e)| Entity {
593 label: if e.name.is_empty() {
594 format!("Edge {i}")
595 } else {
596 e.name.clone()
597 },
598 selected: !e.name.is_empty() && sel_edges.contains(&e.name),
599 visible: s.visibility.is_edge_visible(i),
600 kind: EntityKind::Edge(e.name.clone()),
601 })
602 .collect();
603 let vertices = s
604 .vertices
605 .iter()
606 .enumerate()
607 .map(|(i, v)| Entity {
608 label: format!("Vertex {i}"),
609 selected: sel_vertices.iter().any(|r| {
610 r.solid == s.name
611 && (r.position[0] - v.position[0]).abs() <= TOL
612 && (r.position[1] - v.position[1]).abs() <= TOL
613 && (r.position[2] - v.position[2]).abs() <= TOL
614 }),
615 visible: s.visibility.is_vertex_visible(i),
616 kind: EntityKind::Vertex(v.position),
617 })
618 .collect();
619 SolidRow {
620 name: s.name.clone(),
621 visible: s.visible,
622 selected: sel_solids.contains(&s.name),
623 faces,
624 edges,
625 vertices,
626 }
627 })
628 .collect()
629}
630
631#[cfg(target_arch = "wasm32")]
633fn publish(name: &str, json: &str) {
634 if let Some(win) = web_sys::window() {
635 let _ = js_sys::Reflect::set(
636 &win,
637 &wasm_bindgen::JsValue::from_str(name),
638 &wasm_bindgen::JsValue::from_str(json),
639 );
640 }
641}
642
643#[cfg(test)]
644mod tests {
645 use super::*;
646
647 fn cube_history(name: &str) -> String {
651 serde_json::json!({
652 "expressions": "",
653 "configurator": {},
654 "features": [{
655 "type": "P.CU",
656 "inputParams": {
657 "id": name,
658 "sizeX": 10.0, "sizeY": 10.0, "sizeZ": 10.0,
659 "transform": {
660 "position": [0.0, 0.0, 0.0],
661 "rotationEuler": [0.0, 0.0, 0.0],
662 "scale": [1.0, 1.0, 1.0]
663 },
664 "boolean": { "targets": [], "operation": "NONE" }
665 },
666 "persistentData": {}
667 }]
668 })
669 .to_string()
670 }
671
672 fn run_frame(
677 ctx: &egui::Context,
678 panel: &mut ScenePanel,
679 state: &mut EngineState,
680 events: Vec<egui::Event>,
681 scroll: bool,
682 ) {
683 let raw = egui::RawInput {
684 screen_rect: Some(egui::Rect::from_min_size(
685 egui::pos2(0.0, 0.0),
686 egui::vec2(800.0, 600.0),
687 )),
688 events,
689 ..Default::default()
690 };
691 let _ = ctx.run_ui(raw, |ui| {
692 if scroll {
697 egui::ScrollArea::vertical().show(ui, |ui| panel.show(ui, state));
698 } else {
699 panel.show(ui, state);
700 }
701 });
702 }
703
704 fn click_at(
708 ctx: &egui::Context,
709 panel: &mut ScenePanel,
710 state: &mut EngineState,
711 pos: egui::Pos2,
712 scroll: bool,
713 ) {
714 run_frame(
715 ctx,
716 panel,
717 state,
718 vec![
719 egui::Event::PointerMoved(pos),
720 egui::Event::PointerButton {
721 pos,
722 button: egui::PointerButton::Primary,
723 pressed: true,
724 modifiers: egui::Modifiers::default(),
725 },
726 ],
727 scroll,
728 );
729 run_frame(
730 ctx,
731 panel,
732 state,
733 vec![egui::Event::PointerButton {
734 pos,
735 button: egui::PointerButton::Primary,
736 pressed: false,
737 modifiers: egui::Modifiers::default(),
738 }],
739 scroll,
740 );
741 }
742
743 fn drive_group_checkbox(kind: VisKind, hit_key: &str, scroll: bool) {
750 let ctx = egui::Context::default();
751 let mut state = EngineState::new();
752 state.set_history_json(&cube_history("Box")).unwrap();
753 let mut panel = ScenePanel::new();
754
755 run_frame(&ctx, &mut panel, &mut state, vec![], scroll);
757 assert_eq!(
758 state.group_visibility("Box", kind),
759 Some(GroupState::All),
760 "precondition: every {kind:?} starts visible"
761 );
762 let rect = *panel.hits.get(hit_key).unwrap_or_else(|| {
763 panic!(
764 "missing hit rect {hit_key}; have {:?}",
765 panel.hits.keys().collect::<Vec<_>>()
766 )
767 });
768
769 click_at(&ctx, &mut panel, &mut state, rect.center(), scroll);
771 assert_eq!(
772 state.group_visibility("Box", kind),
773 Some(GroupState::None),
774 "clicking the {kind:?} group checkbox must hide the whole group"
775 );
776
777 run_frame(&ctx, &mut panel, &mut state, vec![], scroll);
779 let rect = *panel.hits.get(hit_key).expect("checkbox rect after hide");
780 click_at(&ctx, &mut panel, &mut state, rect.center(), scroll);
781 assert_eq!(
782 state.group_visibility("Box", kind),
783 Some(GroupState::All),
784 "re-clicking the {kind:?} group checkbox must re-show the whole group"
785 );
786 }
787
788 #[test]
789 fn scene_tree_faces_group_checkbox_hides_and_shows_through_egui() {
790 drive_group_checkbox(VisKind::Face, "vis:Box/Faces", false);
791 }
792
793 #[test]
794 fn scene_tree_edges_group_checkbox_hides_and_shows_through_egui() {
795 drive_group_checkbox(VisKind::Edge, "vis:Box/Edges", false);
796 }
797
798 #[test]
802 fn scene_tree_faces_group_checkbox_works_inside_scroll_area() {
803 drive_group_checkbox(VisKind::Face, "vis:Box/Faces", true);
804 }
805
806 #[test]
811 fn scene_tree_individual_face_checkbox_hides_one_face_through_egui() {
812 let ctx = egui::Context::default();
813 let mut state = EngineState::new();
814 state.set_history_json(&cube_history("Box")).unwrap();
815 let mut panel = ScenePanel::new();
816
817 run_frame(&ctx, &mut panel, &mut state, vec![], false);
819 let box_rect = *panel.hits.get("box:Box/Faces").expect("faces group collapse box");
820
821 click_at(&ctx, &mut panel, &mut state, box_rect.center(), false);
823 run_frame(&ctx, &mut panel, &mut state, vec![], false);
824 assert_eq!(
825 state.entity_visible("Box", VisKind::Face, 0),
826 Some(true),
827 "face 0 starts visible"
828 );
829 let leaf = *panel.hits.get("vis:Box/Faces/0").unwrap_or_else(|| {
830 panic!(
831 "missing face-0 leaf checkbox; have {:?}",
832 panel.hits.keys().collect::<Vec<_>>()
833 )
834 });
835
836 click_at(&ctx, &mut panel, &mut state, leaf.center(), false);
838 assert_eq!(
839 state.entity_visible("Box", VisKind::Face, 0),
840 Some(false),
841 "clicking face 0's checkbox must hide exactly face 0"
842 );
843 assert_eq!(
844 state.entity_visible("Box", VisKind::Face, 1),
845 Some(true),
846 "sibling face 1 stays visible"
847 );
848 assert_eq!(
849 state.group_visibility("Box", VisKind::Face),
850 Some(GroupState::Partial),
851 "one hidden face → group reads Partial"
852 );
853 }
854}