siplot/widget/scene_window.rs
1//! [`SceneWindow`] — a composed 3D scene view: toolbar + scene + properties.
2//!
3//! Port of silx `plot3d.SceneWindow.SceneWindow`, which is a `QMainWindow`
4//! composing a `SceneWidget` (central) with a viewpoint toolbar, an interactive
5//! mode toolbar, a `GroupPropertiesWidget` dock, a `ParamTreeView` dock, and a
6//! `PositionInfoWidget`. The siplot analogue composes the parts that are ported:
7//!
8//! - the [`viewpoint_menu`] drop-down (silx `ViewpointToolBar`) in a toolbar row,
9//! - a [`ScalarFieldView`] as the central scene,
10//! - a [`ScalarFieldProperties`] panel (silx `GroupPropertiesWidget`) in a
11//! toggleable side column,
12//! - a [`ScenePositionInfo`] readout (silx `PositionInfoWidget`) in a bottom
13//! row, fed each frame from the cursor pick over the scene.
14//!
15//! Not composed (deferred upstream, documented in the roadmap): the generic
16//! `ParamTreeView` (`plot3d._model`).
17//!
18//! Like the other plot3d widgets, geometry is uploaded eagerly when the data
19//! layer changes; `ui` only lays the parts out and paints.
20
21use egui::{Response, Ui};
22use egui_wgpu::RenderState;
23
24use crate::core::scene3d::interaction::window_to_ndc;
25use crate::render::gpu_scene3d::Scene3dId;
26use crate::widget::scalar_field_properties::ScalarFieldProperties;
27use crate::widget::scalar_field_view::ScalarFieldView;
28use crate::widget::scene_position_info::ScenePositionInfo;
29use crate::widget::scene_widget::viewpoint_menu;
30
31/// Default width (points) of the properties side column.
32const PROPERTIES_WIDTH: f32 = 200.0;
33
34/// A composed 3D scalar-field window: a viewpoint toolbar above a
35/// [`ScalarFieldView`], with a toggleable [`ScalarFieldProperties`] side panel.
36/// Construct with [`SceneWindow::new`], push data through
37/// [`view_mut`](SceneWindow::view_mut), then call [`show`](SceneWindow::show)
38/// each frame.
39pub struct SceneWindow {
40 view: ScalarFieldView,
41 properties: ScalarFieldProperties,
42 /// Whether the properties side panel is shown (silx tabs the GroupProperties
43 /// dock; here it is a toggle).
44 show_properties: bool,
45 /// Cursor position/value readout (silx `PositionInfoWidget` dock), fed each
46 /// frame from the scene hover.
47 position_info: ScenePositionInfo,
48}
49
50impl SceneWindow {
51 /// Create a scene window bound to `id`, installing the 3D GPU resources into
52 /// `render_state` if needed. Starts empty with the properties panel shown.
53 pub fn new(render_state: &RenderState, id: Scene3dId) -> Self {
54 Self {
55 view: ScalarFieldView::new(render_state, id),
56 properties: ScalarFieldProperties::new(),
57 show_properties: true,
58 position_info: ScenePositionInfo::new(),
59 }
60 }
61
62 /// Read-only access to the central scalar-field view.
63 pub fn view(&self) -> &ScalarFieldView {
64 &self.view
65 }
66
67 /// Mutable access to the central scalar-field view (e.g. to set its data or
68 /// iso-surfaces).
69 pub fn view_mut(&mut self) -> &mut ScalarFieldView {
70 &mut self.view
71 }
72
73 /// Mutable access to the properties panel state.
74 pub fn properties_mut(&mut self) -> &mut ScalarFieldProperties {
75 &mut self.properties
76 }
77
78 /// Read-only access to the cursor position/value readout (silx
79 /// `getPositionInfoWidget`).
80 pub fn position_info(&self) -> &ScenePositionInfo {
81 &self.position_info
82 }
83
84 /// Whether the properties side panel is shown.
85 pub fn properties_visible(&self) -> bool {
86 self.show_properties
87 }
88
89 /// Show or hide the properties side panel.
90 pub fn set_properties_visible(&mut self, visible: bool) {
91 self.show_properties = visible;
92 }
93
94 /// Lay out the toolbar, optional properties column, and scene, handling
95 /// interaction and painting. Returns the egui [`Response`] of the scene rect.
96 pub fn show(&mut self, ui: &mut Ui, render_state: &RenderState) -> Response {
97 // Toolbar (top): viewpoint presets + a properties-panel toggle.
98 egui::Panel::top(ui.id().with("scene_window_toolbar")).show_inside(ui, |ui| {
99 ui.horizontal(|ui| {
100 viewpoint_menu(ui, self.view.scene_mut());
101 ui.checkbox(&mut self.show_properties, "Properties");
102 });
103 });
104
105 // Properties (left), when shown.
106 if self.show_properties {
107 egui::Panel::left(ui.id().with("scene_window_properties"))
108 .default_size(PROPERTIES_WIDTH)
109 .show_inside(ui, |ui| {
110 self.properties.ui(ui, &mut self.view, render_state);
111 });
112 }
113
114 // Position/value readout (bottom). Shows the previous frame's pick — the
115 // scene rect it picks against is only known after the central panel lays
116 // out, so the update below feeds the next frame (one-frame lag, the
117 // idiomatic egui immediate-mode trade-off).
118 egui::Panel::bottom(ui.id().with("scene_window_position_info")).show_inside(ui, |ui| {
119 self.position_info.ui(ui);
120 });
121
122 // Scene fills the rest.
123 let response = egui::CentralPanel::default()
124 .show_inside(ui, |ui| self.view.show(ui))
125 .inner;
126
127 // Update the readout from the cursor over the scene (silx
128 // `PositionInfoWidget.updateInfo` picks at the cursor position).
129 if let Some(pos) = response.hover_pos() {
130 let rect = response.rect;
131 let ppp = ui.ctx().pixels_per_point();
132 let local = ((pos.x - rect.min.x) * ppp, (pos.y - rect.min.y) * ppp);
133 let size_px = (
134 (rect.width() * ppp).max(1.0),
135 (rect.height() * ppp).max(1.0),
136 );
137 let ndc = window_to_ndc(local, size_px);
138 self.position_info.set(self.view.pick(ndc));
139 } else {
140 self.position_info.clear();
141 }
142
143 response
144 }
145}