runmat_plot/core/
plot_renderer.rs

1//! Unified plot rendering pipeline for both interactive GUI and static export
2//!
3//! This module provides the core rendering logic that is shared between
4//! interactive plotting windows and static file exports, ensuring consistent
5//! high-quality output across all use cases.
6
7use crate::core::{Camera, Scene, WgpuRenderer};
8use crate::plots::Figure;
9use glam::{Mat4, Vec3, Vec4};
10use std::sync::Arc;
11
12/// Unified plot renderer that handles both interactive and static rendering
13pub struct PlotRenderer {
14    /// WGPU renderer for GPU-accelerated rendering
15    pub wgpu_renderer: WgpuRenderer,
16
17    /// Current scene being rendered
18    pub scene: Scene,
19
20    /// Camera for view transformations
21    pub camera: Camera,
22
23    /// Current theme configuration  
24    pub theme: crate::styling::PlotThemeConfig,
25
26    /// Cached rendering state
27    data_bounds: Option<(f64, f64, f64, f64)>,
28    needs_update: bool,
29}
30
31/// Configuration for plot rendering
32#[derive(Debug, Clone)]
33pub struct PlotRenderConfig {
34    /// Output dimensions
35    pub width: u32,
36    pub height: u32,
37
38    /// Background color
39    pub background_color: Vec4,
40
41    /// Whether to draw grid
42    pub show_grid: bool,
43
44    /// Whether to draw axes
45    pub show_axes: bool,
46
47    /// Whether to draw title
48    pub show_title: bool,
49
50    /// Anti-aliasing samples
51    pub msaa_samples: u32,
52
53    /// Theme to use
54    pub theme: crate::styling::PlotThemeConfig,
55}
56
57impl Default for PlotRenderConfig {
58    fn default() -> Self {
59        Self {
60            width: 800,
61            height: 600,
62            background_color: Vec4::new(0.08, 0.09, 0.11, 1.0), // Dark theme background
63            show_grid: true,
64            show_axes: true,
65            show_title: true,
66            msaa_samples: 4,
67            theme: crate::styling::PlotThemeConfig::default(),
68        }
69    }
70}
71
72/// Result of rendering operation
73#[derive(Debug)]
74pub struct RenderResult {
75    /// Whether rendering was successful
76    pub success: bool,
77
78    /// Rendered data bounds
79    pub data_bounds: Option<(f64, f64, f64, f64)>,
80
81    /// Performance metrics
82    pub vertex_count: usize,
83    pub triangle_count: usize,
84    pub render_time_ms: f64,
85}
86
87impl PlotRenderer {
88    /// Create a new plot renderer
89    pub async fn new(
90        device: Arc<wgpu::Device>,
91        queue: Arc<wgpu::Queue>,
92        surface_config: wgpu::SurfaceConfiguration,
93    ) -> Result<Self, Box<dyn std::error::Error>> {
94        let wgpu_renderer = WgpuRenderer::new(device, queue, surface_config).await;
95        let scene = Scene::new();
96        let camera = Self::create_default_camera();
97        let theme = crate::styling::PlotThemeConfig::default();
98
99        Ok(Self {
100            wgpu_renderer,
101            scene,
102            camera,
103            theme,
104            data_bounds: None,
105            needs_update: true,
106        })
107    }
108
109    /// Set the figure to render
110    pub fn set_figure(&mut self, figure: Figure) {
111        // Clear existing scene
112        self.scene.clear();
113
114        // Convert figure to scene nodes
115        self.add_figure_to_scene(figure);
116
117        // Mark for update
118        self.needs_update = true;
119    }
120
121    /// Add a figure to the current scene
122    fn add_figure_to_scene(&mut self, mut figure: Figure) {
123        use crate::core::SceneNode;
124
125        // Convert figure to render data first, then create scene nodes
126        let render_data_list = figure.render_data();
127
128        for (node_id_counter, render_data) in render_data_list.into_iter().enumerate() {
129            // Create scene node for this plot element
130            let node = SceneNode {
131                id: node_id_counter as u64,
132                name: format!("Plot {node_id_counter}"),
133                transform: Mat4::IDENTITY,
134                visible: true,
135                cast_shadows: false,
136                receive_shadows: false,
137                parent: None,
138                children: Vec::new(),
139                render_data: Some(render_data),
140                bounds: crate::core::BoundingBox::default(),
141                lod_levels: Vec::new(),
142                current_lod: 0,
143            };
144
145            self.scene.add_node(node);
146        }
147
148        // Update camera to fit data
149        // println!("Scene now has {} visible nodes", self.scene.get_visible_nodes().len());
150        self.fit_camera_to_data();
151    }
152
153    /// Calculate data bounds from scene
154    pub fn calculate_data_bounds(&mut self) -> Option<(f64, f64, f64, f64)> {
155        let mut min_x = f64::INFINITY;
156        let mut max_x = f64::NEG_INFINITY;
157        let mut min_y = f64::INFINITY;
158        let mut max_y = f64::NEG_INFINITY;
159
160        for node in self.scene.get_visible_nodes() {
161            if let Some(render_data) = &node.render_data {
162                for vertex in &render_data.vertices {
163                    let x = vertex.position[0] as f64;
164                    let y = vertex.position[1] as f64;
165                    min_x = min_x.min(x);
166                    max_x = max_x.max(x);
167                    min_y = min_y.min(y);
168                    max_y = max_y.max(y);
169                }
170            }
171        }
172
173        if min_x != f64::INFINITY && max_x != f64::NEG_INFINITY {
174            // Add 10% margin around data for better visualization
175            let x_range = (max_x - min_x).max(0.1);
176            let y_range = (max_y - min_y).max(0.1);
177            let x_margin = x_range * 0.1;
178            let y_margin = y_range * 0.1;
179
180            let bounds = (
181                min_x - x_margin,
182                max_x + x_margin,
183                min_y - y_margin,
184                max_y + y_margin,
185            );
186
187            // println!("Calculated data bounds: {:?}", bounds); // Too noisy
188            self.data_bounds = Some(bounds);
189            Some(bounds)
190        } else {
191            self.data_bounds = None;
192            None
193        }
194    }
195
196    /// Fit camera to show all data
197    pub fn fit_camera_to_data(&mut self) {
198        if let Some((x_min, x_max, y_min, y_max)) = self.calculate_data_bounds() {
199            // Update camera projection to match data bounds
200            if let crate::core::camera::ProjectionType::Orthographic {
201                ref mut left,
202                ref mut right,
203                ref mut bottom,
204                ref mut top,
205                ..
206            } = self.camera.projection
207            {
208                // TEMP: Use fixed bounds to test projection matrix
209                *left = -2.0;
210                *right = 4.0;
211                *bottom = -2.0;
212                *top = 4.0;
213
214                println!(
215                    "CAMERA: Set orthographic bounds: left={}, right={}, bottom={}, top={}",
216                    *left, *right, *bottom, *top
217                );
218            }
219
220            // Center camera to look at data center
221            let center_x = (x_min + x_max) / 2.0;
222            let center_y = (y_min + y_max) / 2.0;
223            self.camera.position = Vec3::new(center_x as f32, center_y as f32, 5.0);
224            self.camera.target = Vec3::new(center_x as f32, center_y as f32, 0.0);
225        }
226    }
227
228    /// Render the current scene to a specific viewport within a texture/surface
229    pub fn render_to_viewport(
230        &mut self,
231        encoder: &mut wgpu::CommandEncoder,
232        target_view: &wgpu::TextureView,
233        _viewport: (f32, f32, f32, f32), // (x, y, width, height) in framebuffer coordinates
234        clear_background: bool,
235        background_color: Option<glam::Vec4>,
236    ) -> Result<RenderResult, Box<dyn std::error::Error>> {
237        let start_time = std::time::Instant::now();
238
239        // Collect render data and create buffers first
240        let mut render_items = Vec::new();
241        let mut total_vertices = 0;
242        let mut total_triangles = 0;
243
244        for node in self.scene.get_visible_nodes() {
245            if let Some(render_data) = &node.render_data {
246                if !render_data.vertices.is_empty() {
247                    // Ensure pipeline exists
248                    self.wgpu_renderer
249                        .ensure_pipeline(render_data.pipeline_type);
250
251                    // Create vertex buffer
252                    let vertex_buffer = self
253                        .wgpu_renderer
254                        .create_vertex_buffer(&render_data.vertices);
255
256                    // Debug: Count vertices being sent to GPU
257                    if render_data.vertices.len() == 12 {
258                        println!(
259                            "CRITICAL: {} vertices -> GPU, draw calls: {}",
260                            render_data.vertices.len(),
261                            render_data.draw_calls.len()
262                        );
263                        for (i, call) in render_data.draw_calls.iter().enumerate() {
264                            println!(
265                                "  Call {}: offset={}, count={}",
266                                i, call.vertex_offset, call.vertex_count
267                            );
268                        }
269                    }
270
271                    render_items.push((render_data, vertex_buffer));
272                    total_vertices += render_data.vertices.len();
273
274                    // Count triangles based on pipeline type
275                    match render_data.pipeline_type {
276                        crate::core::PipelineType::Triangles => {
277                            total_triangles += render_data.vertices.len() / 3;
278                        }
279                        _ => {
280                            // Other pipeline types don't count as triangles
281                        }
282                    }
283                }
284            }
285        }
286
287        // Update uniforms
288        let view_proj_matrix = self.camera.view_proj_matrix();
289
290        self.wgpu_renderer
291            .update_uniforms(view_proj_matrix, Mat4::IDENTITY);
292
293        // Create render pass
294        let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
295            label: Some("Viewport Plot Render Pass"),
296            color_attachments: &[Some(wgpu::RenderPassColorAttachment {
297                view: target_view,
298                resolve_target: None,
299                ops: wgpu::Operations {
300                    load: if clear_background {
301                        wgpu::LoadOp::Clear(wgpu::Color {
302                            r: background_color.map_or(0.08, |c| c.x as f64),
303                            g: background_color.map_or(0.09, |c| c.y as f64),
304                            b: background_color.map_or(0.11, |c| c.z as f64),
305                            a: background_color.map_or(1.0, |c| c.w as f64),
306                        })
307                    } else {
308                        wgpu::LoadOp::Load
309                    },
310                    store: wgpu::StoreOp::Store,
311                },
312            })],
313            depth_stencil_attachment: None,
314            occlusion_query_set: None,
315            timestamp_writes: None,
316        });
317
318        // TEMP: Disable viewport to test if that's causing triangle collapse
319        // let (viewport_x, viewport_y, viewport_width, viewport_height) = _viewport;
320        // render_pass.set_viewport(viewport_x, viewport_y, viewport_width, viewport_height, 0.0, 1.0);
321
322        // Render all items
323        for (render_data, vertex_buffer) in &render_items {
324            let pipeline = self.wgpu_renderer.get_pipeline(render_data.pipeline_type);
325            println!(
326                "RENDER: Using {:?} pipeline for {} vertices",
327                render_data.pipeline_type,
328                render_data.vertices.len()
329            );
330            render_pass.set_pipeline(pipeline);
331            render_pass.set_bind_group(0, self.wgpu_renderer.get_uniform_bind_group(), &[]);
332            render_pass.set_vertex_buffer(0, vertex_buffer.slice(..));
333
334            // Render using draw calls from render_data
335            for draw_call in &render_data.draw_calls {
336                render_pass.draw(
337                    draw_call.vertex_offset as u32
338                        ..(draw_call.vertex_offset + draw_call.vertex_count) as u32,
339                    0..draw_call.instance_count as u32,
340                );
341            }
342        }
343
344        drop(render_pass);
345
346        let render_time = start_time.elapsed().as_secs_f64() * 1000.0;
347
348        Ok(RenderResult {
349            success: true,
350            data_bounds: self.data_bounds,
351            vertex_count: total_vertices,
352            triangle_count: total_triangles,
353            render_time_ms: render_time,
354        })
355    }
356
357    /// High-performance direct viewport rendering with optimized coordinate transformation
358    /// Provides precise data-to-screen mapping for interactive plot windows
359    pub fn render_direct_to_viewport(
360        &mut self,
361        encoder: &mut wgpu::CommandEncoder,
362        target_view: &wgpu::TextureView,
363        viewport: (f32, f32, f32, f32), // (x, y, width, height) in framebuffer coordinates
364        data_bounds: (f64, f64, f64, f64), // (x_min, y_min, x_max, y_max) in data space
365        clear_background: bool,
366        background_color: Option<glam::Vec4>,
367    ) -> Result<RenderResult, Box<dyn std::error::Error>> {
368        let start_time = std::time::Instant::now();
369
370        // Ensure direct line pipeline exists
371        self.wgpu_renderer.ensure_direct_line_pipeline();
372
373        // Calculate viewport NDC bounds
374        let window_width = self.wgpu_renderer.surface_config.width as f32;
375        let window_height = self.wgpu_renderer.surface_config.height as f32;
376
377        let (viewport_x, viewport_y, viewport_width, viewport_height) = viewport;
378
379        // Convert viewport to NDC coordinates
380        let ndc_left = (viewport_x / window_width) * 2.0 - 1.0;
381        let ndc_right = ((viewport_x + viewport_width) / window_width) * 2.0 - 1.0;
382        let ndc_top = 1.0 - (viewport_y / window_height) * 2.0;
383        let ndc_bottom = 1.0 - ((viewport_y + viewport_height) / window_height) * 2.0;
384
385        // Configure shader uniforms for direct coordinate transformation
386        self.wgpu_renderer.update_direct_uniforms(
387            [data_bounds.0 as f32, data_bounds.2 as f32], // data_min (x_min, y_min)
388            [data_bounds.1 as f32, data_bounds.3 as f32], // data_max (x_max, y_max)
389            [ndc_left, ndc_bottom],                       // viewport_min (NDC)
390            [ndc_right, ndc_top],                         // viewport_max (NDC)
391        );
392
393        // Collect render data
394        let mut render_items = Vec::new();
395        let mut total_vertices = 0;
396
397        for node in self.scene.get_visible_nodes() {
398            if let Some(render_data) = &node.render_data {
399                if !render_data.vertices.is_empty() {
400                    let vertex_buffer = self
401                        .wgpu_renderer
402                        .create_vertex_buffer(&render_data.vertices);
403                    render_items.push((render_data, vertex_buffer));
404                    total_vertices += render_data.vertices.len();
405                }
406            }
407        }
408
409        // Create render pass
410        let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
411            label: Some("Direct Viewport Plot Render Pass"),
412            color_attachments: &[Some(wgpu::RenderPassColorAttachment {
413                view: target_view,
414                resolve_target: None,
415                ops: wgpu::Operations {
416                    load: if clear_background {
417                        wgpu::LoadOp::Clear(wgpu::Color {
418                            r: background_color.map_or(0.08, |c| c.x as f64),
419                            g: background_color.map_or(0.09, |c| c.y as f64),
420                            b: background_color.map_or(0.11, |c| c.z as f64),
421                            a: background_color.map_or(1.0, |c| c.w as f64),
422                        })
423                    } else {
424                        wgpu::LoadOp::Load
425                    },
426                    store: wgpu::StoreOp::Store,
427                },
428            })],
429            depth_stencil_attachment: None,
430            timestamp_writes: None,
431            occlusion_query_set: None,
432        });
433
434        // Execute optimized rendering pipeline with pre-transformed coordinates
435        for (render_data, vertex_buffer) in &render_items {
436            // Use direct line pipeline for all line rendering
437            if let Some(pipeline) = &self.wgpu_renderer.direct_line_pipeline {
438                render_pass.set_pipeline(pipeline);
439                render_pass.set_bind_group(0, &self.wgpu_renderer.direct_uniform_bind_group, &[]);
440                render_pass.set_vertex_buffer(0, vertex_buffer.slice(..));
441
442                // Draw all vertices as lines
443                for draw_call in &render_data.draw_calls {
444                    render_pass.draw(
445                        draw_call.vertex_offset as u32
446                            ..(draw_call.vertex_offset + draw_call.vertex_count) as u32,
447                        0..1,
448                    );
449                }
450            }
451        }
452
453        drop(render_pass);
454
455        let render_time = start_time.elapsed().as_millis() as f64;
456
457        Ok(RenderResult {
458            success: true,
459            data_bounds: Some(data_bounds),
460            vertex_count: total_vertices,
461            triangle_count: 0,
462            render_time_ms: render_time,
463        })
464    }
465
466    /// Render the current scene to a texture/surface
467    pub fn render(
468        &mut self,
469        encoder: &mut wgpu::CommandEncoder,
470        target_view: &wgpu::TextureView,
471        config: &PlotRenderConfig,
472    ) -> Result<RenderResult, Box<dyn std::error::Error>> {
473        let start_time = std::time::Instant::now();
474
475        // Update camera aspect ratio
476        let aspect_ratio = config.width as f32 / config.height as f32;
477        self.camera.update_aspect_ratio(aspect_ratio);
478
479        // Update WGPU uniforms
480        let view_proj_matrix = self.camera.view_proj_matrix();
481        let model_matrix = Mat4::IDENTITY;
482        self.wgpu_renderer
483            .update_uniforms(view_proj_matrix, model_matrix);
484
485        // Collect all render data and create vertex buffers first (outside render pass)
486        let mut render_items = Vec::new();
487        let mut total_vertices = 0;
488        let mut total_triangles = 0;
489
490        for node in self.scene.get_visible_nodes() {
491            if let Some(render_data) = &node.render_data {
492                if !render_data.vertices.is_empty() {
493                    // Ensure pipeline exists for this render data
494                    self.wgpu_renderer
495                        .ensure_pipeline(render_data.pipeline_type);
496
497                    // Create vertex buffer for this node
498                    let vertex_buffer = self
499                        .wgpu_renderer
500                        .create_vertex_buffer(&render_data.vertices);
501
502                    // Create index buffer if needed
503                    let index_buffer = if let Some(indices) = &render_data.indices {
504                        Some(self.wgpu_renderer.create_index_buffer(indices))
505                    } else {
506                        None
507                    };
508
509                    render_items.push((render_data, vertex_buffer, index_buffer));
510
511                    total_vertices += render_data.vertices.len();
512                    if let Some(indices) = &render_data.indices {
513                        total_triangles += indices.len() / 3;
514                    }
515                }
516            }
517        }
518
519        // Create render pass
520        let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
521            label: Some("Plot Render Pass"),
522            color_attachments: &[Some(wgpu::RenderPassColorAttachment {
523                view: target_view,
524                resolve_target: None,
525                ops: wgpu::Operations {
526                    load: wgpu::LoadOp::Clear(wgpu::Color {
527                        r: config.background_color.x as f64,
528                        g: config.background_color.y as f64,
529                        b: config.background_color.z as f64,
530                        a: config.background_color.w as f64,
531                    }),
532                    store: wgpu::StoreOp::Store,
533                },
534            })],
535            depth_stencil_attachment: None,
536            occlusion_query_set: None,
537            timestamp_writes: None,
538        });
539
540        // Now render all items with proper bind group setup
541        for (render_data, vertex_buffer, index_buffer) in &render_items {
542            // Get the appropriate pipeline for this render data (pipeline ensured above)
543            let pipeline = self.wgpu_renderer.get_pipeline(render_data.pipeline_type);
544            render_pass.set_pipeline(pipeline);
545
546            // Set the uniform bind group (required by shaders)
547            render_pass.set_bind_group(0, self.wgpu_renderer.get_uniform_bind_group(), &[]);
548
549            render_pass.set_vertex_buffer(0, vertex_buffer.slice(..));
550
551            if let Some(index_buffer) = index_buffer {
552                render_pass.set_index_buffer(index_buffer.slice(..), wgpu::IndexFormat::Uint32);
553                if let Some(indices) = &render_data.indices {
554                    println!(
555                        "RENDER: Drawing {} indices with triangle pipeline",
556                        indices.len()
557                    );
558                    render_pass.draw_indexed(0..indices.len() as u32, 0, 0..1);
559                }
560            } else {
561                println!("RENDER: Drawing direct vertices - no index buffer");
562                // Use draw_calls from render_data for proper vertex range handling
563                for draw_call in &render_data.draw_calls {
564                    println!("RENDER: Direct draw - vertex_offset={}, vertex_count={}, instance_count={}", 
565                             draw_call.vertex_offset, draw_call.vertex_count, draw_call.instance_count);
566                    render_pass.draw(
567                        draw_call.vertex_offset as u32
568                            ..(draw_call.vertex_offset + draw_call.vertex_count) as u32,
569                        0..draw_call.instance_count as u32,
570                    );
571                }
572            }
573        }
574
575        drop(render_pass);
576
577        let render_time = start_time.elapsed().as_secs_f64() * 1000.0;
578
579        Ok(RenderResult {
580            success: true,
581            data_bounds: self.data_bounds,
582            vertex_count: total_vertices,
583            triangle_count: total_triangles,
584            render_time_ms: render_time,
585        })
586    }
587
588    /// Create default 2D camera for plotting
589    fn create_default_camera() -> Camera {
590        let mut camera = Camera::new();
591        camera.projection = crate::core::camera::ProjectionType::Orthographic {
592            left: -5.0,
593            right: 5.0,
594            bottom: -5.0,
595            top: 5.0,
596            near: 0.1,
597            far: 100.0,
598        };
599        camera.position = Vec3::new(0.0, 0.0, 5.0);
600        camera.target = Vec3::new(0.0, 0.0, 0.0);
601        camera.up = Vec3::new(0.0, 1.0, 0.0);
602        camera
603    }
604
605    /// Get current data bounds
606    pub fn data_bounds(&self) -> Option<(f64, f64, f64, f64)> {
607        self.data_bounds
608    }
609
610    /// Get camera reference
611    pub fn camera(&self) -> &Camera {
612        &self.camera
613    }
614
615    /// Get mutable camera reference
616    pub fn camera_mut(&mut self) -> &mut Camera {
617        &mut self.camera
618    }
619
620    /// Get scene reference
621    pub fn scene(&self) -> &Scene {
622        &self.scene
623    }
624
625    /// Get scene statistics
626    pub fn scene_statistics(&self) -> crate::core::SceneStatistics {
627        self.scene.statistics()
628    }
629}
630
631/// High-level plotting utilities that use the unified renderer
632pub mod plot_utils {
633
634    /// Calculate nice tick intervals for axis labeling
635    pub fn calculate_tick_interval(range: f64) -> f64 {
636        let magnitude = 10.0_f64.powf(range.log10().floor());
637        let normalized = range / magnitude;
638
639        let nice_interval = if normalized <= 1.0 {
640            0.2
641        } else if normalized <= 2.0 {
642            0.5
643        } else if normalized <= 5.0 {
644            1.0
645        } else {
646            2.0
647        };
648
649        nice_interval * magnitude
650    }
651
652    /// Format a tick label value for display
653    pub fn format_tick_label(value: f64) -> String {
654        if value.abs() < 0.001 {
655            "0".to_string()
656        } else if value.abs() >= 1000.0 || value.fract().abs() < 0.001 {
657            format!("{value:.0}")
658        } else {
659            format!("{value:.1}")
660        }
661    }
662
663    /// Generate grid lines for plotting
664    pub fn generate_grid_lines(
665        bounds: (f64, f64, f64, f64),
666        plot_rect: (f32, f32, f32, f32), // (left, right, bottom, top)
667    ) -> Vec<(f32, f32, f32, f32)> {
668        // Vector of (x1, y1, x2, y2) line segments
669        let (x_min, x_max, y_min, y_max) = bounds;
670        let (left, right, bottom, top) = plot_rect;
671
672        let mut lines = Vec::new();
673
674        // X-axis grid lines
675        let x_range = x_max - x_min;
676        let x_interval = calculate_tick_interval(x_range);
677        let mut x_val = (x_min / x_interval).ceil() * x_interval;
678
679        while x_val <= x_max {
680            let x_screen = left + ((x_val - x_min) / x_range) as f32 * (right - left);
681            lines.push((x_screen, bottom, x_screen, top));
682            x_val += x_interval;
683        }
684
685        // Y-axis grid lines
686        let y_range = y_max - y_min;
687        let y_interval = calculate_tick_interval(y_range);
688        let mut y_val = (y_min / y_interval).ceil() * y_interval;
689
690        while y_val <= y_max {
691            let y_screen = bottom + ((y_val - y_min) / y_range) as f32 * (top - bottom);
692            lines.push((left, y_screen, right, y_screen));
693            y_val += y_interval;
694        }
695
696        lines
697    }
698}