1use egui::{CollapsingHeader, Context, DragValue, SidePanel, Slider, Ui};
4
5#[derive(Debug, Clone)]
7pub struct CameraSettings {
8 pub navigation_style: u32,
10 pub projection_mode: u32,
12 pub up_direction: u32,
15 pub fov_degrees: f32,
17 pub near: f32,
19 pub far: f32,
21 pub move_speed: f32,
23 pub ortho_scale: f32,
25}
26
27impl Default for CameraSettings {
28 fn default() -> Self {
29 Self {
30 navigation_style: 0, projection_mode: 0, up_direction: 2, fov_degrees: 45.0,
34 near: 0.01,
35 far: 1000.0,
36 move_speed: 1.0,
37 ortho_scale: 1.0,
38 }
39 }
40}
41
42#[derive(Debug, Clone, Default)]
44pub struct SceneExtents {
45 pub auto_compute: bool,
47 pub length_scale: f32,
49 pub bbox_min: [f32; 3],
51 pub bbox_max: [f32; 3],
53}
54
55#[derive(Debug, Clone)]
57pub struct AppearanceSettings {
58 pub transparency_mode: u32,
60 pub ssaa_factor: u32,
62 pub max_fps: u32,
64 pub ssao_enabled: bool,
66 pub ssao_radius: f32,
68 pub ssao_intensity: f32,
70 pub ssao_bias: f32,
72 pub ssao_sample_count: u32,
74}
75
76impl Default for AppearanceSettings {
77 fn default() -> Self {
78 Self {
79 transparency_mode: 1, ssaa_factor: 1,
81 max_fps: 60,
82 ssao_enabled: false,
83 ssao_radius: 0.5,
84 ssao_intensity: 1.5,
85 ssao_bias: 0.025,
86 ssao_sample_count: 32,
87 }
88 }
89}
90
91#[derive(Debug, Clone)]
93pub struct ToneMappingSettings {
94 pub exposure: f32,
96 pub white_level: f32,
98 pub gamma: f32,
100}
101
102impl Default for ToneMappingSettings {
103 fn default() -> Self {
104 Self {
105 exposure: 1.1,
106 white_level: 1.0,
107 gamma: 2.2,
108 }
109 }
110}
111
112#[derive(Debug, Clone)]
114pub struct SlicePlaneSettings {
115 pub name: String,
117 pub enabled: bool,
119 pub origin: [f32; 3],
121 pub normal: [f32; 3],
123 pub draw_plane: bool,
125 pub draw_widget: bool,
127 pub color: [f32; 3],
129 pub transparency: f32,
131 pub plane_size: f32,
133 pub is_selected: bool,
135}
136
137impl Default for SlicePlaneSettings {
138 fn default() -> Self {
139 Self {
140 name: String::new(),
141 enabled: true,
142 origin: [0.0, 0.0, 0.0],
143 normal: [0.0, 1.0, 0.0],
144 draw_plane: true,
145 draw_widget: true,
146 color: [0.5, 0.5, 0.5],
147 transparency: 0.3,
148 plane_size: 0.05,
149 is_selected: false,
150 }
151 }
152}
153
154impl SlicePlaneSettings {
155 pub fn with_name(name: impl Into<String>) -> Self {
157 Self {
158 name: name.into(),
159 ..Default::default()
160 }
161 }
162}
163
164#[derive(Debug, Clone, Default)]
166pub struct SlicePlaneSelectionInfo {
167 pub has_selection: bool,
169 pub name: String,
171 pub origin: [f32; 3],
173 pub rotation_degrees: [f32; 3],
175}
176
177#[derive(Debug, Clone, PartialEq, Default)]
179pub enum SlicePlaneGizmoAction {
180 #[default]
182 None,
183 SelectionChanged,
185 TransformChanged,
187 Deselect,
189}
190
191#[derive(Debug, Clone)]
193pub struct GroupSettings {
194 pub name: String,
196 pub enabled: bool,
198 pub show_child_details: bool,
200 pub parent_group: Option<String>,
202 pub child_structures: Vec<(String, String)>,
204 pub child_groups: Vec<String>,
206}
207
208impl Default for GroupSettings {
209 fn default() -> Self {
210 Self {
211 name: String::new(),
212 enabled: true,
213 show_child_details: true,
214 parent_group: None,
215 child_structures: Vec::new(),
216 child_groups: Vec::new(),
217 }
218 }
219}
220
221impl GroupSettings {
222 pub fn with_name(name: impl Into<String>) -> Self {
224 Self {
225 name: name.into(),
226 ..Default::default()
227 }
228 }
229}
230
231#[derive(Debug, Clone, PartialEq, Eq)]
233pub enum GroupsAction {
234 None,
236 SyncEnabled(Vec<usize>),
238}
239
240#[derive(Debug, Clone)]
242pub struct GizmoSettings {
243 pub local_space: bool,
245 pub visible: bool,
247 pub snap_translate: f32,
249 pub snap_rotate: f32,
251 pub snap_scale: f32,
253}
254
255impl Default for GizmoSettings {
256 fn default() -> Self {
257 Self {
258 local_space: false, visible: true,
260 snap_translate: 0.0,
261 snap_rotate: 0.0,
262 snap_scale: 0.0,
263 }
264 }
265}
266
267#[derive(Debug, Clone, Default)]
269pub struct SelectionInfo {
270 pub has_selection: bool,
272 pub type_name: String,
274 pub name: String,
276 pub translation: [f32; 3],
278 pub rotation_degrees: [f32; 3],
280 pub scale: [f32; 3],
282 pub centroid: [f32; 3],
284}
285
286#[derive(Debug, Clone, PartialEq)]
288pub enum GizmoAction {
289 None,
291 SettingsChanged,
293 TransformChanged,
295 Deselect,
297 ResetTransform,
299}
300
301#[derive(Debug, Clone, PartialEq, Eq)]
303pub enum ViewAction {
304 None,
306 ResetView,
308 Screenshot,
310}
311
312#[derive(Debug, Clone, PartialEq)]
314pub enum MaterialAction {
315 None,
317 LoadStatic { name: String, path: String },
319 LoadBlendable {
321 name: String,
322 base_path: String,
323 extension: String,
324 },
325}
326
327#[derive(Debug, Clone, Default)]
329pub struct MaterialLoadState {
330 pub name: String,
332 pub path: String,
334 pub status: String,
336}
337
338pub fn build_gizmo_section(
341 ui: &mut Ui,
342 settings: &mut GizmoSettings,
343 selection: &mut SelectionInfo,
344) -> GizmoAction {
345 let mut action = GizmoAction::None;
346
347 CollapsingHeader::new("Transform")
348 .default_open(false)
349 .show(ui, |ui| {
350 if selection.has_selection {
352 ui.horizontal(|ui| {
353 ui.label(format!("[{}] {}", selection.type_name, selection.name));
354 if ui.small_button("x").clicked() {
355 action = GizmoAction::Deselect;
356 }
357 });
358
359 ui.separator();
360
361 ui.horizontal(|ui| {
364 ui.label("Pos:");
365 let mut changed = false;
366 changed |= ui
367 .add(
368 DragValue::new(&mut selection.translation[0])
369 .speed(0.1)
370 .prefix("X:"),
371 )
372 .changed();
373 changed |= ui
374 .add(
375 DragValue::new(&mut selection.translation[1])
376 .speed(0.1)
377 .prefix("Y:"),
378 )
379 .changed();
380 changed |= ui
381 .add(
382 DragValue::new(&mut selection.translation[2])
383 .speed(0.1)
384 .prefix("Z:"),
385 )
386 .changed();
387 if changed {
388 action = GizmoAction::TransformChanged;
389 }
390 });
391
392 ui.horizontal(|ui| {
394 ui.label("Rot:");
395 let mut changed = false;
396 changed |= ui
397 .add(
398 DragValue::new(&mut selection.rotation_degrees[0])
399 .speed(1.0)
400 .prefix("X:")
401 .suffix("°"),
402 )
403 .changed();
404 changed |= ui
405 .add(
406 DragValue::new(&mut selection.rotation_degrees[1])
407 .speed(1.0)
408 .prefix("Y:")
409 .suffix("°"),
410 )
411 .changed();
412 changed |= ui
413 .add(
414 DragValue::new(&mut selection.rotation_degrees[2])
415 .speed(1.0)
416 .prefix("Z:")
417 .suffix("°"),
418 )
419 .changed();
420 if changed && action == GizmoAction::None {
421 action = GizmoAction::TransformChanged;
422 }
423 });
424
425 ui.horizontal(|ui| {
427 ui.label("Scale:");
428 let mut changed = false;
429 changed |= ui
430 .add(
431 DragValue::new(&mut selection.scale[0])
432 .speed(0.01)
433 .prefix("X:")
434 .range(0.01..=100.0),
435 )
436 .changed();
437 changed |= ui
438 .add(
439 DragValue::new(&mut selection.scale[1])
440 .speed(0.01)
441 .prefix("Y:")
442 .range(0.01..=100.0),
443 )
444 .changed();
445 changed |= ui
446 .add(
447 DragValue::new(&mut selection.scale[2])
448 .speed(0.01)
449 .prefix("Z:")
450 .range(0.01..=100.0),
451 )
452 .changed();
453 if changed && action == GizmoAction::None {
454 action = GizmoAction::TransformChanged;
455 }
456 });
457
458 ui.horizontal(|ui| {
459 if ui.button("Reset").clicked() {
460 action = GizmoAction::ResetTransform;
461 }
462 ui.separator();
463 if ui.checkbox(&mut settings.visible, "Gizmo").changed() {
464 action = GizmoAction::SettingsChanged;
465 }
466 if settings.visible {
467 if ui
468 .selectable_label(!settings.local_space, "W")
469 .on_hover_text("World space")
470 .clicked()
471 {
472 settings.local_space = false;
473 action = GizmoAction::SettingsChanged;
474 }
475 if ui
476 .selectable_label(settings.local_space, "L")
477 .on_hover_text("Local space")
478 .clicked()
479 {
480 settings.local_space = true;
481 action = GizmoAction::SettingsChanged;
482 }
483 }
484 });
485 } else {
486 ui.label("No selection");
487 }
488 });
489
490 action
491}
492
493fn count_structures_recursive(idx: usize, groups: &[GroupSettings]) -> usize {
495 let mut total = groups[idx].child_structures.len();
496 let name = &groups[idx].name;
497 for (i, g) in groups.iter().enumerate() {
498 if g.parent_group.as_deref() == Some(name) {
499 total += count_structures_recursive(i, groups);
500 }
501 }
502 total
503}
504
505fn collect_descendant_indices(idx: usize, groups: &[GroupSettings], out: &mut Vec<usize>) {
507 let name = &groups[idx].name;
508 for (i, g) in groups.iter().enumerate() {
509 if g.parent_group.as_deref() == Some(name) {
510 out.push(i);
511 collect_descendant_indices(i, groups, out);
512 }
513 }
514}
515
516fn build_group_tree(
518 ui: &mut Ui,
519 idx: usize,
520 groups: &mut Vec<GroupSettings>,
521 toggled_idx: &mut Option<usize>,
522) {
523 let member_count = count_structures_recursive(idx, groups);
524 let label = format!("{} ({member_count})", groups[idx].name);
525
526 ui.horizontal(|ui| {
527 if ui.checkbox(&mut groups[idx].enabled, label).changed() && toggled_idx.is_none() {
528 *toggled_idx = Some(idx);
529 }
530 });
531
532 let child_name = groups[idx].name.clone();
534 let child_indices: Vec<usize> = groups
535 .iter()
536 .enumerate()
537 .filter(|(_, g)| g.parent_group.as_deref() == Some(child_name.as_str()))
538 .map(|(i, _)| i)
539 .collect();
540
541 if !child_indices.is_empty() {
542 ui.indent(format!("group_children_{idx}"), |ui| {
543 for child_idx in child_indices {
544 build_group_tree(ui, child_idx, groups, toggled_idx);
545 }
546 });
547 }
548}
549
550pub fn build_groups_section(ui: &mut Ui, groups: &mut Vec<GroupSettings>) -> GroupsAction {
557 if groups.is_empty() {
558 return GroupsAction::None;
559 }
560
561 let mut toggled_idx: Option<usize> = None;
562
563 CollapsingHeader::new("Groups")
564 .default_open(true)
565 .show(ui, |ui| {
566 let root_indices: Vec<usize> = groups
568 .iter()
569 .enumerate()
570 .filter(|(_, g)| g.parent_group.is_none())
571 .map(|(i, _)| i)
572 .collect();
573
574 for idx in root_indices {
575 build_group_tree(ui, idx, groups, &mut toggled_idx);
576 }
577 });
578
579 if let Some(idx) = toggled_idx {
580 let new_state = groups[idx].enabled;
582 let mut affected = vec![idx];
583 collect_descendant_indices(idx, groups, &mut affected);
584 for &i in &affected[1..] {
585 groups[i].enabled = new_state;
586 }
587 GroupsAction::SyncEnabled(affected)
588 } else {
589 GroupsAction::None
590 }
591}
592
593pub fn build_left_panel(ctx: &Context, build_contents: impl FnOnce(&mut Ui)) -> f32 {
596 let resp = SidePanel::left("polyscope_main_panel")
597 .default_width(305.0)
598 .resizable(true)
599 .show(ctx, |ui| {
600 ui.heading("polyscope-rs");
601 ui.separator();
602 egui::ScrollArea::vertical()
603 .auto_shrink([false; 2])
604 .show(ui, |ui| {
605 build_contents(ui);
606 });
607 });
608 resp.response.rect.width()
609}
610
611pub fn build_controls_section(ui: &mut Ui, background_color: &mut [f32; 3]) -> ViewAction {
614 let mut action = ViewAction::None;
615
616 CollapsingHeader::new("View")
617 .default_open(false)
618 .show(ui, |ui| {
619 ui.horizontal(|ui| {
620 ui.label("Background:");
621 ui.color_edit_button_rgb(background_color);
622 });
623
624 ui.columns(2, |cols| {
625 let w = cols[0].available_width();
626 let h = cols[0].spacing().interact_size.y;
627 if cols[0]
628 .add_sized([w, h], egui::Button::new("Reset View"))
629 .clicked()
630 {
631 action = ViewAction::ResetView;
632 }
633 if cols[1]
634 .add_sized([w, h], egui::Button::new("Screenshot"))
635 .clicked()
636 {
637 action = ViewAction::Screenshot;
638 }
639 });
640
641 ui.label("Tip: Press F12 for quick screenshot");
642 });
643
644 action
645}
646
647pub fn build_camera_settings_section(ui: &mut Ui, settings: &mut CameraSettings) -> bool {
650 let mut changed = false;
651
652 CollapsingHeader::new("Camera")
653 .default_open(false)
654 .show(ui, |ui| {
655 egui::ComboBox::from_label("Navigation")
657 .selected_text(match settings.navigation_style {
658 0 => "Turntable",
659 1 => "Free",
660 2 => "Planar",
661 3 => "Arcball",
662 4 => "First Person",
663 _ => "None",
664 })
665 .show_ui(ui, |ui| {
666 for (i, name) in [
667 "Turntable",
668 "Free",
669 "Planar",
670 "Arcball",
671 "First Person",
672 "None",
673 ]
674 .iter()
675 .enumerate()
676 {
677 if ui
678 .selectable_value(&mut settings.navigation_style, i as u32, *name)
679 .changed()
680 {
681 changed = true;
682 }
683 }
684 });
685
686 egui::ComboBox::from_label("Projection")
688 .selected_text(if settings.projection_mode == 0 {
689 "Perspective"
690 } else {
691 "Orthographic"
692 })
693 .show_ui(ui, |ui| {
694 if ui
695 .selectable_value(&mut settings.projection_mode, 0, "Perspective")
696 .changed()
697 {
698 changed = true;
699 }
700 if ui
701 .selectable_value(&mut settings.projection_mode, 1, "Orthographic")
702 .changed()
703 {
704 changed = true;
705 }
706 });
707
708 ui.separator();
709
710 let directions = ["+X", "-X", "+Y", "-Y", "+Z", "-Z"];
712 let front_for_up = ["+Y", "-Y", "-Z", "+Z", "+X", "-X"];
714
715 egui::ComboBox::from_label("Up")
716 .selected_text(directions[settings.up_direction as usize])
717 .show_ui(ui, |ui| {
718 for (i, name) in directions.iter().enumerate() {
719 if ui
720 .selectable_value(&mut settings.up_direction, i as u32, *name)
721 .changed()
722 {
723 changed = true;
724 }
725 }
726 });
727
728 ui.horizontal(|ui| {
730 ui.label("Front:");
731 ui.label(front_for_up[settings.up_direction as usize]);
732 ui.label("(auto)");
733 });
734
735 ui.separator();
736
737 if settings.projection_mode == 0 {
739 ui.horizontal(|ui| {
740 ui.label("FOV:");
741 if ui
742 .add(Slider::new(&mut settings.fov_degrees, 10.0..=170.0).suffix("°"))
743 .changed()
744 {
745 changed = true;
746 }
747 });
748 } else {
749 ui.horizontal(|ui| {
751 ui.label("Scale:");
752 if ui
753 .add(
754 DragValue::new(&mut settings.ortho_scale)
755 .speed(0.1)
756 .range(0.1..=100.0),
757 )
758 .changed()
759 {
760 changed = true;
761 }
762 });
763 }
764
765 ui.horizontal(|ui| {
767 ui.label("Near:");
768 if ui
769 .add(
770 DragValue::new(&mut settings.near)
771 .speed(0.001)
772 .range(0.001..=10.0),
773 )
774 .changed()
775 {
776 changed = true;
777 }
778 });
779
780 ui.horizontal(|ui| {
781 ui.label("Far:");
782 if ui
783 .add(
784 DragValue::new(&mut settings.far)
785 .speed(1.0)
786 .range(10.0..=10000.0),
787 )
788 .changed()
789 {
790 changed = true;
791 }
792 });
793
794 ui.horizontal(|ui| {
796 ui.label("Move Speed:");
797 if ui
798 .add(
799 DragValue::new(&mut settings.move_speed)
800 .speed(0.1)
801 .range(0.1..=10.0),
802 )
803 .changed()
804 {
805 changed = true;
806 }
807 });
808 });
809
810 changed
811}
812
813pub fn build_scene_extents_section(ui: &mut Ui, extents: &mut SceneExtents) -> bool {
816 let mut changed = false;
817
818 CollapsingHeader::new("Scene Extents")
819 .default_open(false)
820 .show(ui, |ui| {
821 if ui
822 .checkbox(&mut extents.auto_compute, "Auto-compute")
823 .changed()
824 {
825 changed = true;
826 }
827
828 ui.separator();
829
830 if extents.auto_compute {
831 ui.horizontal(|ui| {
833 ui.label("Length scale:");
834 ui.label(format!("{:.4}", extents.length_scale));
835 });
836
837 ui.label("Bounding box:");
838 ui.indent("bbox", |ui| {
839 ui.horizontal(|ui| {
840 ui.label("Min:");
841 ui.label(format!(
842 "({:.2}, {:.2}, {:.2})",
843 extents.bbox_min[0], extents.bbox_min[1], extents.bbox_min[2]
844 ));
845 });
846 ui.horizontal(|ui| {
847 ui.label("Max:");
848 ui.label(format!(
849 "({:.2}, {:.2}, {:.2})",
850 extents.bbox_max[0], extents.bbox_max[1], extents.bbox_max[2]
851 ));
852 });
853 });
854 } else {
855 ui.horizontal(|ui| {
857 ui.label("Length scale:");
858 if ui
859 .add(
860 DragValue::new(&mut extents.length_scale)
861 .speed(0.01)
862 .range(0.0001..=f32::MAX),
863 )
864 .changed()
865 {
866 changed = true;
867 }
868 });
869
870 ui.label("Bounding box:");
871 ui.indent("bbox_edit", |ui| {
872 ui.horizontal(|ui| {
873 ui.label("Min:");
874 for val in &mut extents.bbox_min {
875 if ui.add(DragValue::new(val).speed(0.01)).changed() {
876 changed = true;
877 }
878 }
879 });
880 ui.horizontal(|ui| {
881 ui.label("Max:");
882 for val in &mut extents.bbox_max {
883 if ui.add(DragValue::new(val).speed(0.01)).changed() {
884 changed = true;
885 }
886 }
887 });
888 });
889 }
890
891 let center = [
893 f32::midpoint(extents.bbox_min[0], extents.bbox_max[0]),
894 f32::midpoint(extents.bbox_min[1], extents.bbox_max[1]),
895 f32::midpoint(extents.bbox_min[2], extents.bbox_max[2]),
896 ];
897 ui.horizontal(|ui| {
898 ui.label("Center:");
899 ui.label(format!(
900 "({:.2}, {:.2}, {:.2})",
901 center[0], center[1], center[2]
902 ));
903 });
904 });
905
906 changed
907}
908
909pub fn build_appearance_section(ui: &mut Ui, settings: &mut AppearanceSettings) -> bool {
912 let mut changed = false;
913
914 CollapsingHeader::new("Appearance")
915 .default_open(false)
916 .show(ui, |ui| {
917 egui::ComboBox::from_label("Transparency")
919 .selected_text(match settings.transparency_mode {
920 0 => "None",
921 1 => "Simple",
922 _ => "Pretty",
923 })
924 .show_ui(ui, |ui| {
925 if ui
926 .selectable_value(&mut settings.transparency_mode, 0, "None")
927 .changed()
928 {
929 changed = true;
930 }
931 if ui
932 .selectable_value(&mut settings.transparency_mode, 1, "Simple")
933 .changed()
934 {
935 changed = true;
936 }
937 if ui
938 .selectable_value(&mut settings.transparency_mode, 2, "Pretty")
939 .changed()
940 {
941 changed = true;
942 }
943 });
944
945 ui.separator();
946
947 egui::ComboBox::from_label("Anti-aliasing")
949 .selected_text(format!("{}x SSAA", settings.ssaa_factor))
950 .show_ui(ui, |ui| {
951 if ui
952 .selectable_value(&mut settings.ssaa_factor, 1, "1x (Off)")
953 .changed()
954 {
955 changed = true;
956 }
957 if ui
958 .selectable_value(&mut settings.ssaa_factor, 2, "2x SSAA")
959 .changed()
960 {
961 changed = true;
962 }
963 if ui
964 .selectable_value(&mut settings.ssaa_factor, 4, "4x SSAA")
965 .changed()
966 {
967 changed = true;
968 }
969 });
970
971 ui.separator();
972
973 ui.horizontal(|ui| {
975 ui.label("Max FPS:");
976 let mut fps = settings.max_fps as i32;
977 if ui.add(DragValue::new(&mut fps).range(0..=240)).changed() {
978 settings.max_fps = fps.max(0) as u32;
979 changed = true;
980 }
981 if settings.max_fps == 0 {
982 ui.label("(unlimited)");
983 }
984 });
985
986 ui.separator();
987
988 if ui.checkbox(&mut settings.ssao_enabled, "SSAO").changed() {
990 changed = true;
991 }
992
993 if settings.ssao_enabled {
994 egui::Grid::new("ssao_grid").num_columns(2).show(ui, |ui| {
995 ui.label("Radius:");
996 if ui
997 .add(Slider::new(&mut settings.ssao_radius, 0.01..=2.0))
998 .changed()
999 {
1000 changed = true;
1001 }
1002 ui.end_row();
1003
1004 ui.label("Intensity:");
1005 if ui
1006 .add(Slider::new(&mut settings.ssao_intensity, 0.1..=3.0))
1007 .changed()
1008 {
1009 changed = true;
1010 }
1011 ui.end_row();
1012
1013 ui.label("Bias:");
1014 if ui
1015 .add(Slider::new(&mut settings.ssao_bias, 0.001..=0.1))
1016 .changed()
1017 {
1018 changed = true;
1019 }
1020 ui.end_row();
1021
1022 ui.label("Samples:");
1023 let mut samples = settings.ssao_sample_count as i32;
1024 if ui.add(DragValue::new(&mut samples).range(4..=64)).changed() {
1025 settings.ssao_sample_count = samples.max(4) as u32;
1026 changed = true;
1027 }
1028 ui.end_row();
1029 });
1030 }
1031 });
1032
1033 changed
1034}
1035
1036pub fn build_material_section(ui: &mut Ui, state: &mut MaterialLoadState) -> MaterialAction {
1039 let mut action = MaterialAction::None;
1040
1041 CollapsingHeader::new("Materials")
1042 .default_open(false)
1043 .show(ui, |ui| {
1044 egui::Grid::new("material_load_grid")
1045 .num_columns(2)
1046 .show(ui, |ui| {
1047 ui.label("Name:");
1048 ui.text_edit_singleline(&mut state.name);
1049 ui.end_row();
1050
1051 ui.label("File path:");
1052 ui.text_edit_singleline(&mut state.path);
1053 ui.end_row();
1054 });
1055
1056 ui.horizontal(|ui| {
1057 if ui.button("Load Static").clicked()
1058 && !state.name.is_empty()
1059 && !state.path.is_empty()
1060 {
1061 action = MaterialAction::LoadStatic {
1062 name: state.name.clone(),
1063 path: state.path.clone(),
1064 };
1065 }
1066 if ui.button("Load Blendable").clicked()
1067 && !state.name.is_empty()
1068 && !state.path.is_empty()
1069 {
1070 let p = std::path::Path::new(&state.path);
1072 let ext = p
1073 .extension()
1074 .map(|e| format!(".{}", e.to_string_lossy()))
1075 .unwrap_or_default();
1076 let base = state
1077 .path
1078 .strip_suffix(&ext)
1079 .unwrap_or(&state.path)
1080 .to_string();
1081 action = MaterialAction::LoadBlendable {
1082 name: state.name.clone(),
1083 base_path: base,
1084 extension: ext,
1085 };
1086 }
1087 });
1088
1089 if !state.status.is_empty() {
1090 ui.label(&state.status);
1091 }
1092 });
1093
1094 action
1095}
1096
1097pub fn build_tone_mapping_section(ui: &mut Ui, settings: &mut ToneMappingSettings) -> bool {
1100 let mut changed = false;
1101
1102 CollapsingHeader::new("Tone Mapping")
1103 .default_open(false)
1104 .show(ui, |ui| {
1105 egui::Grid::new("tone_mapping_grid")
1106 .num_columns(2)
1107 .show(ui, |ui| {
1108 ui.label("Exposure:");
1109 if ui
1110 .add(
1111 Slider::new(&mut settings.exposure, 0.1..=4.0)
1112 .logarithmic(true)
1113 .clamping(egui::SliderClamping::Always),
1114 )
1115 .changed()
1116 {
1117 changed = true;
1118 }
1119 ui.end_row();
1120
1121 ui.label("White Level:");
1122 if ui
1123 .add(
1124 Slider::new(&mut settings.white_level, 0.5..=4.0)
1125 .logarithmic(true)
1126 .clamping(egui::SliderClamping::Always),
1127 )
1128 .changed()
1129 {
1130 changed = true;
1131 }
1132 ui.end_row();
1133
1134 ui.label("Gamma:");
1135 if ui
1136 .add(
1137 Slider::new(&mut settings.gamma, 1.0..=3.0)
1138 .clamping(egui::SliderClamping::Always),
1139 )
1140 .changed()
1141 {
1142 changed = true;
1143 }
1144 ui.end_row();
1145 });
1146
1147 ui.separator();
1148 if ui.button("Reset to Defaults").clicked() {
1149 *settings = ToneMappingSettings::default();
1150 changed = true;
1151 }
1152 });
1153
1154 changed
1155}
1156
1157#[derive(Debug, Clone, PartialEq, Eq)]
1159pub enum SlicePlanesAction {
1160 None,
1162 Add(String),
1164 Remove(usize),
1166 Modified(usize),
1168}
1169
1170fn build_slice_plane_item(ui: &mut Ui, settings: &mut SlicePlaneSettings) -> bool {
1173 let mut changed = false;
1174
1175 ui.horizontal(|ui| {
1177 if ui.checkbox(&mut settings.enabled, "Enabled").changed() {
1178 changed = true;
1179 }
1180 });
1181
1182 ui.separator();
1183
1184 ui.label("Origin:");
1186 ui.horizontal(|ui| {
1187 ui.label("X:");
1188 if ui
1189 .add(DragValue::new(&mut settings.origin[0]).speed(0.1))
1190 .changed()
1191 {
1192 changed = true;
1193 }
1194 ui.label("Y:");
1195 if ui
1196 .add(DragValue::new(&mut settings.origin[1]).speed(0.1))
1197 .changed()
1198 {
1199 changed = true;
1200 }
1201 ui.label("Z:");
1202 if ui
1203 .add(DragValue::new(&mut settings.origin[2]).speed(0.1))
1204 .changed()
1205 {
1206 changed = true;
1207 }
1208 });
1209
1210 ui.label("Normal:");
1212 ui.columns(6, |cols| {
1213 let w = cols[0].available_width();
1214 let h = cols[0].spacing().interact_size.y;
1215 if cols[0].add_sized([w, h], egui::Button::new("+X")).clicked() {
1216 settings.normal = [1.0, 0.0, 0.0];
1217 changed = true;
1218 }
1219 if cols[1].add_sized([w, h], egui::Button::new("-X")).clicked() {
1220 settings.normal = [-1.0, 0.0, 0.0];
1221 changed = true;
1222 }
1223 if cols[2].add_sized([w, h], egui::Button::new("+Y")).clicked() {
1224 settings.normal = [0.0, 1.0, 0.0];
1225 changed = true;
1226 }
1227 if cols[3].add_sized([w, h], egui::Button::new("-Y")).clicked() {
1228 settings.normal = [0.0, -1.0, 0.0];
1229 changed = true;
1230 }
1231 if cols[4].add_sized([w, h], egui::Button::new("+Z")).clicked() {
1232 settings.normal = [0.0, 0.0, 1.0];
1233 changed = true;
1234 }
1235 if cols[5].add_sized([w, h], egui::Button::new("-Z")).clicked() {
1236 settings.normal = [0.0, 0.0, -1.0];
1237 changed = true;
1238 }
1239 });
1240
1241 ui.horizontal(|ui| {
1243 ui.label("X:");
1244 if ui
1245 .add(
1246 DragValue::new(&mut settings.normal[0])
1247 .speed(0.01)
1248 .range(-1.0..=1.0),
1249 )
1250 .changed()
1251 {
1252 changed = true;
1253 }
1254 ui.label("Y:");
1255 if ui
1256 .add(
1257 DragValue::new(&mut settings.normal[1])
1258 .speed(0.01)
1259 .range(-1.0..=1.0),
1260 )
1261 .changed()
1262 {
1263 changed = true;
1264 }
1265 ui.label("Z:");
1266 if ui
1267 .add(
1268 DragValue::new(&mut settings.normal[2])
1269 .speed(0.01)
1270 .range(-1.0..=1.0),
1271 )
1272 .changed()
1273 {
1274 changed = true;
1275 }
1276 });
1277
1278 ui.separator();
1279
1280 ui.horizontal(|ui| {
1282 if ui
1283 .checkbox(&mut settings.draw_plane, "Draw plane")
1284 .changed()
1285 {
1286 changed = true;
1287 }
1288 if ui
1289 .checkbox(&mut settings.draw_widget, "Draw widget")
1290 .changed()
1291 {
1292 changed = true;
1293 }
1294 });
1295
1296 if settings.draw_widget && settings.enabled {
1298 let gizmo_text = if settings.is_selected {
1299 "Deselect Gizmo"
1300 } else {
1301 "Edit with Gizmo"
1302 };
1303 if ui.button(gizmo_text).clicked() {
1304 settings.is_selected = !settings.is_selected;
1305 changed = true;
1306 }
1307 }
1308
1309 egui::Grid::new("slice_plane_props")
1311 .num_columns(2)
1312 .show(ui, |ui| {
1313 ui.label("Plane size:");
1314 if ui
1315 .add(Slider::new(&mut settings.plane_size, 0.01..=1.0).logarithmic(true))
1316 .changed()
1317 {
1318 changed = true;
1319 }
1320 ui.end_row();
1321
1322 ui.label("Color:");
1323 if ui.color_edit_button_rgb(&mut settings.color).changed() {
1324 changed = true;
1325 }
1326 ui.end_row();
1327 });
1328
1329 changed
1330}
1331
1332pub fn build_slice_planes_section(
1335 ui: &mut Ui,
1336 planes: &mut Vec<SlicePlaneSettings>,
1337 new_plane_name: &mut String,
1338) -> SlicePlanesAction {
1339 let mut action = SlicePlanesAction::None;
1340
1341 CollapsingHeader::new("Slice Planes")
1342 .default_open(false)
1343 .show(ui, |ui| {
1344 ui.horizontal(|ui| {
1346 ui.label("New plane:");
1347 ui.add_sized([80.0, 18.0], egui::TextEdit::singleline(new_plane_name));
1348 if ui.button("Add").clicked() && !new_plane_name.is_empty() {
1349 action = SlicePlanesAction::Add(new_plane_name.clone());
1350 }
1351 });
1352
1353 if planes.is_empty() {
1354 ui.label("No slice planes");
1355 return;
1356 }
1357
1358 ui.separator();
1359
1360 let mut remove_idx = None;
1362 for (idx, plane) in planes.iter_mut().enumerate() {
1363 let header_text =
1364 format!("{} {}", if plane.enabled { "●" } else { "○" }, plane.name);
1365
1366 CollapsingHeader::new(header_text)
1367 .id_salt(format!("slice_plane_{idx}"))
1368 .default_open(false)
1369 .show(ui, |ui| {
1370 if build_slice_plane_item(ui, plane) && action == SlicePlanesAction::None {
1371 action = SlicePlanesAction::Modified(idx);
1372 }
1373
1374 ui.separator();
1375 if ui.button("Remove").clicked() {
1376 remove_idx = Some(idx);
1377 }
1378 });
1379 }
1380
1381 if let Some(idx) = remove_idx {
1382 action = SlicePlanesAction::Remove(idx);
1383 }
1384 });
1385
1386 action
1387}
1388
1389pub fn build_ground_plane_section(
1391 ui: &mut Ui,
1392 mode: &mut u32, height: &mut f32,
1394 height_is_relative: &mut bool,
1395 shadow_blur_iters: &mut u32,
1396 shadow_darkness: &mut f32,
1397 reflection_intensity: &mut f32,
1398) -> bool {
1399 let mut changed = false;
1400
1401 CollapsingHeader::new("Ground Plane")
1402 .default_open(false)
1403 .show(ui, |ui| {
1404 egui::ComboBox::from_label("Mode")
1406 .selected_text(match *mode {
1407 0 => "None",
1408 1 => "Tile",
1409 2 => "Shadow Only",
1410 3 => "Tile + Reflection",
1411 _ => "Unknown",
1412 })
1413 .show_ui(ui, |ui| {
1414 if ui.selectable_value(mode, 0, "None").changed() {
1415 changed = true;
1416 }
1417 if ui.selectable_value(mode, 1, "Tile").changed() {
1418 changed = true;
1419 }
1420 if ui.selectable_value(mode, 2, "Shadow Only").changed() {
1421 changed = true;
1422 }
1423 if ui.selectable_value(mode, 3, "Tile + Reflection").changed() {
1424 changed = true;
1425 }
1426 });
1427
1428 if *mode > 0 {
1429 ui.separator();
1430
1431 if ui.checkbox(height_is_relative, "Auto height").changed() {
1433 changed = true;
1434 }
1435
1436 if !*height_is_relative {
1437 ui.horizontal(|ui| {
1438 ui.label("Height:");
1439 if ui.add(egui::DragValue::new(height).speed(0.1)).changed() {
1440 changed = true;
1441 }
1442 });
1443 }
1444
1445 ui.separator();
1447 ui.label("Shadow Settings:");
1448
1449 egui::Grid::new("shadow_grid")
1450 .num_columns(2)
1451 .show(ui, |ui| {
1452 ui.label("Blur iterations:");
1453 if ui.add(Slider::new(shadow_blur_iters, 0..=5)).changed() {
1454 changed = true;
1455 }
1456 ui.end_row();
1457
1458 ui.label("Darkness:");
1459 if ui.add(Slider::new(shadow_darkness, 0.0..=1.0)).changed() {
1460 changed = true;
1461 }
1462 ui.end_row();
1463 });
1464
1465 if *mode == 3 {
1467 ui.separator();
1468 ui.label("Reflection Settings:");
1469
1470 egui::Grid::new("reflection_grid")
1471 .num_columns(2)
1472 .show(ui, |ui| {
1473 ui.label("Intensity:");
1474 if ui
1475 .add(Slider::new(reflection_intensity, 0.0..=1.0))
1476 .changed()
1477 {
1478 changed = true;
1479 }
1480 ui.end_row();
1481 });
1482 }
1483 }
1484 });
1485
1486 changed
1487}
1488
1489pub fn build_structure_tree<F>(
1491 ui: &mut Ui,
1492 structures: &[(String, String, bool)], mut on_toggle: F,
1494) where
1495 F: FnMut(&str, &str, bool), {
1497 CollapsingHeader::new("Structures")
1498 .default_open(true)
1499 .show(ui, |ui| {
1500 if structures.is_empty() {
1501 ui.label("No structures registered");
1502 return;
1503 }
1504
1505 let mut by_type: std::collections::BTreeMap<&str, Vec<(&str, bool)>> =
1507 std::collections::BTreeMap::new();
1508 for (type_name, name, enabled) in structures {
1509 by_type
1510 .entry(type_name.as_str())
1511 .or_default()
1512 .push((name.as_str(), *enabled));
1513 }
1514
1515 for (type_name, instances) in &by_type {
1516 let mut sorted_instances: Vec<_> = instances.iter().collect();
1518 sorted_instances.sort_by_key(|(name, _)| *name);
1519
1520 let header = format!("{} ({})", type_name, instances.len());
1521 CollapsingHeader::new(header)
1522 .default_open(instances.len() <= 8)
1523 .show(ui, |ui| {
1524 for (name, enabled) in sorted_instances {
1525 let mut enabled_mut = *enabled;
1526 ui.horizontal(|ui| {
1527 if ui.checkbox(&mut enabled_mut, "").changed() {
1528 on_toggle(type_name, name, enabled_mut);
1529 }
1530 ui.label(*name);
1531 });
1532 }
1533 });
1534 }
1535 });
1536}
1537
1538pub fn build_structure_tree_with_ui<F, U>(
1543 ui: &mut Ui,
1544 structures: &[(String, String, bool)], mut on_toggle: F,
1546 mut build_ui: U,
1547) where
1548 F: FnMut(&str, &str, bool), U: FnMut(&mut Ui, &str, &str), {
1551 CollapsingHeader::new("Structures")
1552 .default_open(true)
1553 .show(ui, |ui| {
1554 if structures.is_empty() {
1555 ui.label("No structures registered");
1556 return;
1557 }
1558
1559 let mut by_type: std::collections::BTreeMap<&str, Vec<(&str, bool)>> =
1561 std::collections::BTreeMap::new();
1562 for (type_name, name, enabled) in structures {
1563 by_type
1564 .entry(type_name.as_str())
1565 .or_default()
1566 .push((name.as_str(), *enabled));
1567 }
1568
1569 for (type_name, instances) in &by_type {
1570 let mut sorted_instances: Vec<_> = instances.iter().collect();
1572 sorted_instances.sort_by_key(|(name, _)| *name);
1573
1574 let header = format!("{} ({})", type_name, instances.len());
1575 CollapsingHeader::new(header)
1576 .default_open(instances.len() <= 8)
1577 .show(ui, |ui| {
1578 for (name, enabled) in sorted_instances {
1579 let mut enabled_mut = *enabled;
1580
1581 let structure_header = CollapsingHeader::new(*name)
1583 .default_open(false)
1584 .show(ui, |ui| {
1585 if ui.checkbox(&mut enabled_mut, "Enabled").changed() {
1587 on_toggle(type_name, name, enabled_mut);
1588 }
1589
1590 ui.separator();
1591
1592 build_ui(ui, type_name, name);
1594 });
1595
1596 let _ = structure_header.body_returned;
1599 }
1600 });
1601 }
1602 });
1603}