Skip to main content

polyscope_structures/camera_view/
mod.rs

1//! Camera view structure for visualizing camera poses.
2
3mod camera_parameters;
4
5pub use camera_parameters::*;
6
7use glam::{Mat4, Vec3, Vec4};
8use polyscope_core::pick::PickResult;
9use polyscope_core::quantity::Quantity;
10use polyscope_core::structure::{HasQuantities, RenderContext, Structure};
11use polyscope_render::CurveNetworkRenderData;
12
13/// A camera view structure for visualizing camera poses.
14pub struct CameraView {
15    name: String,
16
17    // Camera data
18    params: CameraParameters,
19
20    // Common structure fields
21    enabled: bool,
22    transform: Mat4,
23    quantities: Vec<Box<dyn Quantity>>,
24
25    // Visualization parameters
26    color: Vec4,
27    widget_focal_length: f32,
28    widget_focal_length_is_relative: bool,
29    widget_thickness: f32,
30
31    // GPU resources (reuses CurveNetwork edge rendering)
32    render_data: Option<CurveNetworkRenderData>,
33    /// Length scale used when geometry was last generated (to regenerate if it changes).
34    prepared_length_scale: f32,
35
36    // UI state: set to true when user clicks "fly to" button
37    fly_to_requested: bool,
38}
39
40impl CameraView {
41    /// Creates a new camera view from camera parameters.
42    pub fn new(name: impl Into<String>, params: CameraParameters) -> Self {
43        Self {
44            name: name.into(),
45            params,
46            enabled: true,
47            transform: Mat4::IDENTITY,
48            quantities: Vec::new(),
49            color: Vec4::new(0.0, 0.0, 0.0, 1.0),
50            widget_focal_length: 0.20,
51            widget_focal_length_is_relative: true,
52            widget_thickness: 0.02,
53            render_data: None,
54            prepared_length_scale: 0.0,
55            fly_to_requested: false,
56        }
57    }
58
59    /// Creates a camera view with position and look direction.
60    pub fn from_look_at(
61        name: impl Into<String>,
62        position: Vec3,
63        look_at: Vec3,
64        up: Vec3,
65        fov_vertical_degrees: f32,
66        aspect_ratio: f32,
67    ) -> Self {
68        let look_dir = (look_at - position).normalize();
69        let params = CameraParameters::from_vectors(
70            position,
71            look_dir,
72            up,
73            fov_vertical_degrees,
74            aspect_ratio,
75        );
76        Self::new(name, params)
77    }
78
79    /// Gets the camera parameters.
80    #[must_use]
81    pub fn params(&self) -> &CameraParameters {
82        &self.params
83    }
84
85    /// Updates the camera parameters.
86    pub fn set_params(&mut self, params: CameraParameters) -> &mut Self {
87        self.params = params;
88        self.render_data = None; // Invalidate cached geometry
89        self
90    }
91
92    /// Gets the widget color.
93    #[must_use]
94    pub fn color(&self) -> Vec4 {
95        self.color
96    }
97
98    /// Sets the widget color.
99    pub fn set_color(&mut self, color: Vec3) -> &mut Self {
100        self.color = color.extend(1.0);
101        // Invalidate cached geometry so it regenerates with the new color
102        self.render_data = None;
103        self
104    }
105
106    /// Gets the widget focal length (distance from camera origin to frame).
107    #[must_use]
108    pub fn widget_focal_length(&self) -> f32 {
109        self.widget_focal_length
110    }
111
112    /// Sets the widget focal length.
113    pub fn set_widget_focal_length(&mut self, length: f32, is_relative: bool) -> &mut Self {
114        self.widget_focal_length = length;
115        self.widget_focal_length_is_relative = is_relative;
116        self.render_data = None; // Invalidate cached geometry
117        self
118    }
119
120    /// Gets the widget thickness (line/sphere radius relative to focal length).
121    #[must_use]
122    pub fn widget_thickness(&self) -> f32 {
123        self.widget_thickness
124    }
125
126    /// Sets the widget thickness.
127    pub fn set_widget_thickness(&mut self, thickness: f32) -> &mut Self {
128        self.widget_thickness = thickness;
129        self.render_data = None; // Invalidate cached geometry
130        self
131    }
132
133    /// Computes the actual focal length based on length scale.
134    fn compute_focal_length(&self, length_scale: f32) -> f32 {
135        if self.widget_focal_length_is_relative {
136            self.widget_focal_length * length_scale
137        } else {
138            self.widget_focal_length
139        }
140    }
141
142    /// Computes the line radius based on focal length.
143    fn compute_radius(&self, length_scale: f32) -> f32 {
144        let focal = self.compute_focal_length(length_scale);
145        focal * self.widget_thickness
146    }
147
148    /// Generates the camera frustum wireframe geometry.
149    fn generate_wireframe(&self, length_scale: f32) -> (Vec<Vec3>, Vec<[u32; 2]>) {
150        let focal = self.compute_focal_length(length_scale);
151
152        let root = self.params.position();
153        let (look_dir, up_dir, right_dir) = self.params.camera_frame();
154
155        // Frame center is at focal distance from camera
156        let frame_center = root + look_dir * focal;
157
158        // Compute frame half-dimensions based on FoV and aspect ratio
159        let half_height = focal * (self.params.fov_vertical_degrees().to_radians() / 2.0).tan();
160        let half_width = self.params.aspect_ratio() * half_height;
161
162        let frame_up = up_dir * half_height;
163        let frame_right = right_dir * half_width;
164
165        // Frame corners
166        let upper_left = frame_center + frame_up - frame_right;
167        let upper_right = frame_center + frame_up + frame_right;
168        let lower_left = frame_center - frame_up - frame_right;
169        let lower_right = frame_center - frame_up + frame_right;
170
171        // Orientation triangle (above frame)
172        let tri_left = frame_center + frame_up * 1.2 - frame_right * 0.7;
173        let tri_right = frame_center + frame_up * 1.2 + frame_right * 0.7;
174        let tri_top = frame_center + frame_up * 2.0;
175
176        // Nodes: 0=root, 1-4=corners, 5-7=triangle
177        let nodes = vec![
178            root,        // 0
179            upper_left,  // 1
180            upper_right, // 2
181            lower_left,  // 3
182            lower_right, // 4
183            tri_left,    // 5
184            tri_right,   // 6
185            tri_top,     // 7
186        ];
187
188        // Edges
189        let edges = vec![
190            // From root to corners
191            [0, 1],
192            [0, 2],
193            [0, 3],
194            [0, 4],
195            // Frame rectangle
196            [1, 2],
197            [2, 4],
198            [4, 3],
199            [3, 1],
200            // Orientation triangle
201            [5, 6],
202            [6, 7],
203            [7, 5],
204        ];
205
206        (nodes, edges)
207    }
208
209    /// Returns true if geometry needs to be (re-)initialized because `length_scale` changed.
210    #[must_use]
211    pub fn needs_reinit(&self, length_scale: f32) -> bool {
212        self.render_data.is_none()
213            || (self.widget_focal_length_is_relative
214                && (self.prepared_length_scale - length_scale).abs() > 1e-6)
215    }
216
217    /// Initializes GPU render data.
218    pub fn init_render_data(
219        &mut self,
220        device: &wgpu::Device,
221        bind_group_layout: &wgpu::BindGroupLayout,
222        camera_buffer: &wgpu::Buffer,
223        queue: &wgpu::Queue,
224        length_scale: f32,
225    ) {
226        let (nodes, edges) = self.generate_wireframe(length_scale);
227
228        // Build edge indices
229        let edge_tail_inds: Vec<u32> = edges.iter().map(|e| e[0]).collect();
230        let edge_tip_inds: Vec<u32> = edges.iter().map(|e| e[1]).collect();
231
232        let render_data = CurveNetworkRenderData::new(
233            device,
234            bind_group_layout,
235            camera_buffer,
236            &nodes,
237            &edge_tail_inds,
238            &edge_tip_inds,
239        );
240
241        // Update uniforms with our custom settings
242        let uniforms = polyscope_render::CurveNetworkUniforms {
243            color: self.color.to_array(),
244            radius: self.compute_radius(length_scale),
245            radius_is_relative: 0, // Absolute radius since we already computed it
246            render_mode: 0,        // Lines
247            _padding: 0.0,
248        };
249        render_data.update_uniforms(queue, &uniforms);
250
251        self.render_data = Some(render_data);
252        self.prepared_length_scale = length_scale;
253    }
254
255    /// Returns the render data if available.
256    #[must_use]
257    pub fn render_data(&self) -> Option<&CurveNetworkRenderData> {
258        self.render_data.as_ref()
259    }
260
261    /// Returns true if the user has requested to fly to this camera view.
262    /// The flag is automatically cleared after reading.
263    #[must_use]
264    pub fn take_fly_to_request(&mut self) -> bool {
265        let requested = self.fly_to_requested;
266        self.fly_to_requested = false;
267        requested
268    }
269
270    /// Builds the egui UI for this camera view.
271    pub fn build_egui_ui(&mut self, ui: &mut egui::Ui) {
272        // "Fly to" button + camera info on same line
273        ui.horizontal(|ui| {
274            if ui.button("fly to").clicked() {
275                self.fly_to_requested = true;
276            }
277            ui.label(format!(
278                "FoV: {:.1}°  aspect: {:.2}",
279                self.params.fov_vertical_degrees(),
280                self.params.aspect_ratio()
281            ));
282        });
283
284        // Color picker
285        ui.horizontal(|ui| {
286            ui.label("Color:");
287            let mut color = [self.color.x, self.color.y, self.color.z];
288            if ui.color_edit_button_rgb(&mut color).changed() {
289                self.set_color(Vec3::new(color[0], color[1], color[2]));
290            }
291        });
292
293        // Widget thickness
294        ui.horizontal(|ui| {
295            ui.label("Thickness:");
296            let mut thickness = self.widget_thickness;
297            if ui
298                .add(
299                    egui::DragValue::new(&mut thickness)
300                        .speed(0.001)
301                        .range(0.001..=0.5),
302                )
303                .changed()
304            {
305                self.set_widget_thickness(thickness);
306            }
307        });
308
309        // Camera info
310        ui.separator();
311        ui.label(format!(
312            "Position: ({:.2}, {:.2}, {:.2})",
313            self.params.position().x,
314            self.params.position().y,
315            self.params.position().z
316        ));
317    }
318}
319
320impl Structure for CameraView {
321    fn as_any(&self) -> &dyn std::any::Any {
322        self
323    }
324
325    fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
326        self
327    }
328
329    fn name(&self) -> &str {
330        &self.name
331    }
332
333    fn type_name(&self) -> &'static str {
334        "CameraView"
335    }
336
337    fn bounding_box(&self) -> Option<(Vec3, Vec3)> {
338        // Bounding box is just the camera position
339        // The frustum extends based on length scale which we don't have here
340        Some((self.params.position(), self.params.position()))
341    }
342
343    fn length_scale(&self) -> f32 {
344        // No obvious length scale for a camera
345        0.0
346    }
347
348    fn transform(&self) -> Mat4 {
349        self.transform
350    }
351
352    fn set_transform(&mut self, transform: Mat4) {
353        self.transform = transform;
354        // Note: transform is applied in world space, render_data will need refresh
355    }
356
357    fn is_enabled(&self) -> bool {
358        self.enabled
359    }
360
361    fn set_enabled(&mut self, enabled: bool) {
362        self.enabled = enabled;
363    }
364
365    fn draw(&self, _ctx: &mut dyn RenderContext) {
366        // Drawing is handled externally using render_data()
367    }
368
369    fn draw_pick(&self, _ctx: &mut dyn RenderContext) {
370        // Picking not yet implemented
371    }
372
373    fn build_ui(&mut self, _ui: &dyn std::any::Any) {
374        // UI is built via build_egui_ui
375    }
376
377    fn build_pick_ui(&self, _ui: &dyn std::any::Any, _pick: &PickResult) {
378        // Pick UI not implemented
379    }
380
381    fn clear_gpu_resources(&mut self) {
382        self.render_data = None;
383        for quantity in &mut self.quantities {
384            quantity.clear_gpu_resources();
385        }
386    }
387
388    fn refresh(&mut self) {
389        // Invalidate render data so it will be regenerated
390        self.render_data = None;
391        for quantity in &mut self.quantities {
392            quantity.refresh();
393        }
394    }
395}
396
397impl HasQuantities for CameraView {
398    fn add_quantity(&mut self, quantity: Box<dyn Quantity>) {
399        self.quantities.push(quantity);
400    }
401
402    fn get_quantity(&self, name: &str) -> Option<&dyn Quantity> {
403        self.quantities
404            .iter()
405            .find(|q| q.name() == name)
406            .map(std::convert::AsRef::as_ref)
407    }
408
409    fn get_quantity_mut(&mut self, name: &str) -> Option<&mut Box<dyn Quantity>> {
410        self.quantities.iter_mut().find(|q| q.name() == name)
411    }
412
413    fn remove_quantity(&mut self, name: &str) -> Option<Box<dyn Quantity>> {
414        let idx = self.quantities.iter().position(|q| q.name() == name)?;
415        Some(self.quantities.remove(idx))
416    }
417
418    fn quantities(&self) -> &[Box<dyn Quantity>] {
419        &self.quantities
420    }
421}