Skip to main content

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}