1use crate::core::renderer::Vertex;
8use crate::core::{Camera, ClipPolicy, DepthMode, Scene, WgpuRenderer};
9use crate::plots::figure::{LegendEntry, TextStyle};
10use crate::plots::surface::ColorMap;
11use crate::plots::Figure;
12use glam::{Mat4, Vec3, Vec4};
13use runmat_time::Instant;
14use std::cell::RefCell;
15use std::collections::HashMap;
16use std::sync::Arc;
17
18#[derive(Clone, Debug)]
19struct CachedSceneBuffers {
20 vertex_signature: (usize, usize),
21 vertex_buffer: Arc<wgpu::Buffer>,
22 index_signature: Option<(usize, usize)>,
23 index_buffer: Option<Arc<wgpu::Buffer>>,
24}
25
26pub struct PlotRenderer {
28 pub wgpu_renderer: WgpuRenderer,
30
31 pub scene: Scene,
33
34 pub theme: crate::styling::PlotThemeConfig,
36
37 data_bounds: Option<(f64, f64, f64, f64)>,
39 needs_update: bool,
40
41 figure_title: Option<String>,
43 figure_x_label: Option<String>,
44 figure_y_label: Option<String>,
45 figure_z_label: Option<String>,
46 figure_show_grid: bool,
47 figure_show_legend: bool,
48 figure_show_box: bool,
49 figure_x_limits: Option<(f64, f64)>,
50 figure_y_limits: Option<(f64, f64)>,
51 legend_entries: Vec<LegendEntry>,
52 figure_x_log: bool,
53 figure_y_log: bool,
54 figure_axis_equal: bool,
55 figure_colormap: ColorMap,
56 figure_colorbar_enabled: bool,
57 figure_categorical_is_x: Option<bool>,
59 figure_categorical_labels: Option<Vec<String>>,
60 axes_cameras: Vec<Camera>,
62 pub(crate) last_figure: Option<crate::plots::Figure>,
64
65 last_scene_viewport_px: Option<(u32, u32)>,
68 last_axes_plot_sizes_px: Option<Vec<(u32, u32)>>,
70
71 camera_auto_fit: bool,
73 axes_2d_camera_user_controlled: Vec<bool>,
76 scene_buffer_cache: RefCell<HashMap<u64, CachedSceneBuffers>>,
78}
79
80#[derive(Debug, Clone)]
82pub struct PlotRenderConfig {
83 pub width: u32,
85 pub height: u32,
86
87 pub background_color: Vec4,
89
90 pub show_grid: bool,
92
93 pub show_axes: bool,
95
96 pub show_title: bool,
98
99 pub msaa_samples: u32,
101
102 pub depth_mode: DepthMode,
104
105 pub clip_policy: ClipPolicy,
107
108 pub theme: crate::styling::PlotThemeConfig,
110}
111
112impl Default for PlotRenderConfig {
113 fn default() -> Self {
114 Self {
115 width: 800,
116 height: 600,
117 background_color: Vec4::new(0.08, 0.09, 0.11, 1.0), show_grid: true,
119 show_axes: true,
120 show_title: true,
121 msaa_samples: 4,
122 depth_mode: DepthMode::default(),
123 clip_policy: ClipPolicy::default(),
124 theme: crate::styling::PlotThemeConfig::default(),
125 }
126 }
127}
128
129pub struct RenderTarget<'a> {
131 pub view: &'a wgpu::TextureView,
132 pub resolve_target: Option<&'a wgpu::TextureView>,
133}
134
135#[derive(Debug)]
137pub struct RenderResult {
138 pub success: bool,
140
141 pub data_bounds: Option<(f64, f64, f64, f64)>,
143
144 pub vertex_count: usize,
146 pub triangle_count: usize,
147 pub render_time_ms: f64,
148}
149
150impl PlotRenderer {
151 pub fn on_surface_config_updated(&mut self) {
156 let current = (
157 self.wgpu_renderer.surface_config.width.max(1),
158 self.wgpu_renderer.surface_config.height.max(1),
159 );
160 if self.last_scene_viewport_px == Some(current) {
161 return;
162 }
163 let Some(figure) = self.last_figure.clone() else {
164 self.last_scene_viewport_px = Some(current);
165 return;
166 };
167 self.set_figure(figure);
169 }
170
171 fn prepare_buffers_for_render_data(
172 &self,
173 node_id: u64,
174 render_data: &crate::core::RenderData,
175 ) -> Option<(Arc<wgpu::Buffer>, Option<Arc<wgpu::Buffer>>)> {
176 let mut cache = self.scene_buffer_cache.borrow_mut();
177 let vertex_signature = (
178 render_data.vertices.as_ptr() as usize,
179 render_data.vertices.len(),
180 );
181 let index_signature = render_data
182 .indices
183 .as_ref()
184 .map(|indices| (indices.as_ptr() as usize, indices.len()));
185
186 if let Some(cached) = cache.get(&node_id) {
187 if cached.vertex_signature == vertex_signature
188 && cached.index_signature == index_signature
189 {
190 return Some((cached.vertex_buffer.clone(), cached.index_buffer.clone()));
191 }
192 }
193
194 let vertex_buffer = self
195 .wgpu_renderer
196 .vertex_buffer_from_sources(render_data.gpu_vertices.as_ref(), &render_data.vertices)?;
197 let index_buffer = render_data
198 .indices
199 .as_ref()
200 .map(|indices| Arc::new(self.wgpu_renderer.create_index_buffer(indices)));
201
202 cache.insert(
203 node_id,
204 CachedSceneBuffers {
205 vertex_signature,
206 vertex_buffer: vertex_buffer.clone(),
207 index_signature,
208 index_buffer: index_buffer.clone(),
209 },
210 );
211
212 Some((vertex_buffer, index_buffer))
213 }
214
215 fn gpu_indirect_args(render_data: &crate::core::RenderData) -> Option<(&wgpu::Buffer, u64)> {
216 render_data
217 .gpu_vertices
218 .as_ref()
219 .and_then(|buf| buf.indirect.as_ref())
220 .map(|indirect| (indirect.args.as_ref(), indirect.offset))
221 }
222
223 pub async fn new(
225 device: Arc<wgpu::Device>,
226 queue: Arc<wgpu::Queue>,
227 surface_config: wgpu::SurfaceConfiguration,
228 ) -> Result<Self, Box<dyn std::error::Error>> {
229 let wgpu_renderer = WgpuRenderer::new(device, queue, surface_config).await;
230 let scene = Scene::new();
231 let theme = crate::styling::PlotThemeConfig::default();
232
233 Ok(Self {
234 wgpu_renderer,
235 scene,
236 theme,
237 data_bounds: None,
238 needs_update: true,
239 figure_title: None,
240 figure_x_label: None,
241 figure_y_label: None,
242 figure_z_label: None,
243 figure_show_grid: true,
244 figure_show_legend: true,
245 figure_show_box: true,
246 figure_x_limits: None,
247 figure_y_limits: None,
248 legend_entries: Vec::new(),
249 figure_x_log: false,
250 figure_y_log: false,
251 figure_axis_equal: false,
252 figure_colormap: ColorMap::Parula,
253 figure_colorbar_enabled: false,
254 figure_categorical_is_x: None,
255 figure_categorical_labels: None,
256 axes_cameras: vec![Self::create_default_camera()],
257 last_figure: None,
258 last_scene_viewport_px: None,
259 last_axes_plot_sizes_px: None,
260 camera_auto_fit: true,
261 axes_2d_camera_user_controlled: vec![false],
262 scene_buffer_cache: RefCell::new(HashMap::new()),
263 })
264 }
265
266 fn plot_element_is_3d(plot: &crate::plots::figure::PlotElement) -> bool {
267 match plot {
268 crate::plots::figure::PlotElement::Surface(surface) => !surface.image_mode,
269 crate::plots::figure::PlotElement::Line3(_) => true,
270 crate::plots::figure::PlotElement::Scatter3(_) => true,
271 _ => false,
272 }
273 }
274
275 pub fn axes_has_3d_content(&self, axes_index: usize) -> bool {
276 self.last_figure
277 .as_ref()
278 .map(|figure| {
279 figure
280 .plots()
281 .zip(figure.plot_axes_indices().iter().copied())
282 .any(|(plot, plot_axes_index)| {
283 plot_axes_index == axes_index && Self::plot_element_is_3d(plot)
284 })
285 })
286 .unwrap_or(false)
287 }
288
289 pub fn note_camera_interaction(&mut self) {
291 if self.camera_auto_fit {
292 log::debug!(target: "runmat_plot", "camera_auto_fit disabled (user interaction)");
293 }
294 self.camera_auto_fit = false;
295 }
296
297 pub fn note_axes_camera_interaction(&mut self, axes_index: usize) {
298 self.note_camera_interaction();
299 if self.axes_has_3d_content(axes_index) {
300 return;
301 }
302 if let Some(flag) = self.axes_2d_camera_user_controlled.get_mut(axes_index) {
303 *flag = true;
304 }
305 }
306
307 fn clear_axes_camera_interaction(&mut self, axes_index: usize) {
308 if let Some(flag) = self.axes_2d_camera_user_controlled.get_mut(axes_index) {
309 *flag = false;
310 }
311 }
312
313 fn clear_all_axes_camera_interaction(&mut self) {
314 for flag in &mut self.axes_2d_camera_user_controlled {
315 *flag = false;
316 }
317 }
318
319 pub fn set_figure(&mut self, figure: Figure) {
321 self.scene.clear();
323 self.scene_buffer_cache.borrow_mut().clear();
324
325 self.cache_figure_meta(&figure);
327 self.last_figure = Some(figure.clone());
328 self.last_axes_plot_sizes_px = None;
329 let (rows, cols) = figure.axes_grid();
331 let num_axes = rows.max(1) * cols.max(1);
332
333 if self.axes_cameras.len() != num_axes {
334 self.axes_cameras
335 .resize_with(num_axes, Self::create_default_camera);
336 self.axes_2d_camera_user_controlled.resize(num_axes, false);
337 self.camera_auto_fit = true;
338 }
339
340 for axes_index in 0..num_axes {
341 let wants_3d = self.axes_has_3d_content(axes_index);
342 let has_3d_camera = self
343 .axes_cameras
344 .get(axes_index)
345 .map(|cam| {
346 matches!(
347 cam.projection,
348 crate::core::camera::ProjectionType::Perspective { .. }
349 )
350 })
351 .unwrap_or(false);
352 if wants_3d != has_3d_camera {
353 self.axes_cameras[axes_index] = if wants_3d {
354 Camera::new()
355 } else {
356 Self::create_default_camera()
357 };
358 self.clear_axes_camera_interaction(axes_index);
359 self.camera_auto_fit = true;
360 }
361 }
362
363 self.add_figure_to_scene(figure);
364
365 self.needs_update = true;
367
368 let fit_applied = if self.camera_auto_fit {
370 if num_axes > 1 {
371 self.fit_cameras_to_axes_data()
372 } else {
373 self.fit_camera_to_data()
374 }
375 } else {
376 false
377 };
378 if self.camera_auto_fit && fit_applied {
379 self.camera_auto_fit = false;
382 }
383 self.apply_stored_axes_views();
384 }
385
386 fn add_figure_to_scene(&mut self, figure: Figure) {
388 self.add_figure_to_scene_with_axes_plot_sizes(figure, None);
389 }
390
391 fn add_figure_to_scene_with_axes_plot_sizes(
392 &mut self,
393 mut figure: Figure,
394 axes_plot_sizes_px: Option<&[(u32, u32)]>,
395 ) {
396 use crate::core::SceneNode;
397
398 let viewport_px = (
400 self.wgpu_renderer.surface_config.width.max(1),
401 self.wgpu_renderer.surface_config.height.max(1),
402 );
403 self.last_scene_viewport_px = Some(viewport_px);
404 let gpu = crate::core::GpuPackContext {
405 device: &self.wgpu_renderer.device,
406 queue: &self.wgpu_renderer.queue,
407 };
408 let render_data_list = figure.render_data_with_axes_with_viewport_and_gpu(
409 Some(viewport_px),
410 axes_plot_sizes_px,
411 Some(&gpu),
412 );
413 let (rows, cols) = figure.axes_grid();
414
415 for (node_id_counter, (axes_index, render_data)) in render_data_list.into_iter().enumerate()
416 {
417 let axes_index = axes_index.min(rows * cols - 1);
418 let node = SceneNode {
420 id: node_id_counter as u64,
421 name: format!("Plot {node_id_counter} @axes {axes_index}"),
422 transform: Mat4::IDENTITY,
423 visible: true,
424 cast_shadows: false,
425 receive_shadows: false,
426 axes_index,
427 parent: None,
428 children: Vec::new(),
429 render_data: Some(render_data),
430 bounds: crate::core::BoundingBox::default(),
431 lod_levels: Vec::new(),
432 current_lod: 0,
433 };
434
435 let nid = self.scene.add_node(node);
436 let _ = nid;
438 let _ = axes_index;
439 let _ = rows;
440 let _ = cols;
441 }
442 }
443
444 pub fn ensure_scene_viewport_dependent_geometry_for_axes(
445 &mut self,
446 axes_plot_sizes_px: &[(u32, u32)],
447 ) {
448 let normalized: Vec<(u32, u32)> = axes_plot_sizes_px
449 .iter()
450 .map(|&(w, h)| (w.max(1), h.max(1)))
451 .collect();
452 if self.last_axes_plot_sizes_px.as_ref() == Some(&normalized) {
453 return;
454 }
455 let Some(figure) = self.last_figure.clone() else {
456 self.last_axes_plot_sizes_px = Some(normalized);
457 return;
458 };
459 self.scene.clear();
460 self.scene_buffer_cache.borrow_mut().clear();
461 self.add_figure_to_scene_with_axes_plot_sizes(figure, Some(&normalized));
462 log::debug!(
463 target: "runmat_plot.viewport_rebuild",
464 "rebuilt viewport-dependent scene geometry axes_count={} viewport_sizes={:?}",
465 normalized.len(),
466 normalized
467 );
468 self.refit_2d_cameras_to_scene_bounds();
469 self.last_axes_plot_sizes_px = Some(normalized);
470 self.needs_update = true;
471 }
472
473 fn refit_2d_cameras_to_scene_bounds(&mut self) {
474 for idx in 0..self.axes_cameras.len() {
475 if self.axes_has_3d_content(idx) {
476 continue;
477 }
478 if self
479 .axes_2d_camera_user_controlled
480 .get(idx)
481 .copied()
482 .unwrap_or(false)
483 {
484 continue;
485 }
486 let Some((x_min, x_max, y_min, y_max)) = self.display_bounds_for_axes(idx) else {
487 continue;
488 };
489 let geometry_bounds = self.axes_bounds(idx);
490 let Some(cam) = self.axes_cameras.get_mut(idx) else {
491 continue;
492 };
493 if let crate::core::camera::ProjectionType::Orthographic {
494 ref mut left,
495 ref mut right,
496 ref mut bottom,
497 ref mut top,
498 ..
499 } = cam.projection
500 {
501 *left = x_min as f32;
502 *right = x_max as f32;
503 *bottom = y_min as f32;
504 *top = y_max as f32;
505 let camera_left = *left;
506 let camera_right = *right;
507 let camera_bottom = *bottom;
508 let camera_top = *top;
509 cam.position.z = 1.0;
510 cam.target.z = 0.0;
511 cam.mark_dirty();
512 if let Some(bounds) = geometry_bounds {
513 log::debug!(
514 target: "runmat_plot.camera_refit",
515 "refit 2d camera to rebuilt scene bounds axes_index={} geometry=({}, {})..({}, {}) camera=({}, {})..({}, {}) margins=top:{} bottom:{} left:{} right:{}",
516 idx,
517 bounds.min.x,
518 bounds.min.y,
519 bounds.max.x,
520 bounds.max.y,
521 camera_left,
522 camera_bottom,
523 camera_right,
524 camera_top,
525 camera_top - bounds.max.y,
526 bounds.min.y - camera_bottom,
527 bounds.min.x - camera_left,
528 camera_right - bounds.max.x
529 );
530 } else {
531 log::debug!(
532 target: "runmat_plot.camera_refit",
533 "refit 2d camera without geometry bounds axes_index={} camera=({}, {})..({}, {})",
534 idx,
535 camera_left,
536 camera_bottom,
537 camera_right,
538 camera_top
539 );
540 }
541 if let Some(display_bounds) = self.display_bounds_for_axes(idx) {
542 log::debug!(
543 target: "runmat_plot.bounds_chain",
544 "bounds chain axes_index={} axes_bounds=({}, {})..({}, {}) display_bounds=({}, {})..({}, {}) camera_bounds=({}, {})..({}, {})",
545 idx,
546 geometry_bounds.map(|b| b.min.x as f64).unwrap_or(f64::NAN),
547 geometry_bounds.map(|b| b.min.y as f64).unwrap_or(f64::NAN),
548 geometry_bounds.map(|b| b.max.x as f64).unwrap_or(f64::NAN),
549 geometry_bounds.map(|b| b.max.y as f64).unwrap_or(f64::NAN),
550 display_bounds.0,
551 display_bounds.2,
552 display_bounds.1,
553 display_bounds.3,
554 camera_left,
555 camera_bottom,
556 camera_right,
557 camera_top
558 );
559 }
560 }
561 }
562 }
563
564 fn cache_figure_meta(&mut self, figure: &Figure) {
566 self.figure_title = figure.title.clone();
567 self.figure_x_label = figure.x_label.clone();
568 self.figure_y_label = figure.y_label.clone();
569 self.figure_z_label = figure.z_label.clone();
570 self.figure_show_grid = figure.grid_enabled;
571 self.figure_show_legend = figure.legend_enabled;
572 self.figure_show_box = figure.box_enabled;
573 self.figure_x_limits = figure.x_limits;
574 self.figure_y_limits = figure.y_limits;
575 self.legend_entries = figure.legend_entries();
576 self.figure_x_log = figure.x_log;
577 self.figure_y_log = figure.y_log;
578 self.figure_axis_equal = figure.axis_equal;
579 self.figure_colormap = figure.colormap;
580 self.figure_colorbar_enabled = figure.colorbar_enabled;
581 if let Some((is_x, labels)) = figure.categorical_axis_labels() {
583 self.figure_categorical_is_x = Some(is_x);
584 self.figure_categorical_labels = Some(labels);
585 } else {
586 self.figure_categorical_is_x = None;
587 self.figure_categorical_labels = None;
588 }
589 }
590
591 fn apply_stored_axes_views(&mut self) {
592 let Some(fig) = self.last_figure.as_ref() else {
593 return;
594 };
595 for (idx, cam) in self.axes_cameras.iter_mut().enumerate() {
596 if !matches!(
597 cam.projection,
598 crate::core::camera::ProjectionType::Perspective { .. }
599 ) {
600 continue;
601 }
602 if let Some(meta) = fig.axes_metadata(idx) {
603 if let (Some(az), Some(el)) = (meta.view_azimuth_deg, meta.view_elevation_deg) {
604 cam.set_view_angles_deg(az, el);
605 }
606 }
607 }
608 }
609
610 fn display_bounds_for_axes(&self, axes_index: usize) -> Option<(f64, f64, f64, f64)> {
611 let base = self.axes_bounds(axes_index)?;
612 let mut x_min = base.min.x as f64;
613 let mut x_max = base.max.x as f64;
614 let mut y_min = base.min.y as f64;
615 let mut y_max = base.max.y as f64;
616
617 if let Some(fig) = self.last_figure.as_ref() {
618 if let Some(meta) = fig.axes_metadata(axes_index) {
619 if let Some((xl, xr)) = meta.x_limits {
620 x_min = xl;
621 x_max = xr;
622 }
623 if let Some((yl, yr)) = meta.y_limits {
624 y_min = yl;
625 y_max = yr;
626 }
627 if meta.axis_equal {
628 let cx = (x_min + x_max) * 0.5;
629 let cy = (y_min + y_max) * 0.5;
630 let size = (x_max - x_min).abs().max((y_max - y_min).abs()).max(0.1);
631 x_min = cx - size * 0.5;
632 x_max = cx + size * 0.5;
633 y_min = cy - size * 0.5;
634 y_max = cy + size * 0.5;
635 }
636 }
637 }
638
639 Some((x_min, x_max, y_min, y_max))
640 }
641
642 fn fit_cameras_to_axes_data(&mut self) -> bool {
643 let mut applied = false;
644 for idx in 0..self.axes_cameras.len() {
645 if self.axes_has_3d_content(idx) {
646 let Some(bounds) = self.axes_bounds(idx) else {
647 continue;
648 };
649 let center = (bounds.min + bounds.max) * 0.5;
650 let mut cam = Camera::new();
651 cam.target = center;
652 cam.up = Vec3::Z;
653 cam.position = center + Vec3::new(1.0, -1.0, 1.0);
654 cam.fit_bounds(bounds.min, bounds.max);
655 self.axes_cameras[idx] = cam;
656 applied = true;
657 continue;
658 }
659
660 let Some((x_min, x_max, y_min, y_max)) = self.display_bounds_for_axes(idx) else {
661 continue;
662 };
663 let mut cam = Self::create_default_camera();
664 if let crate::core::camera::ProjectionType::Orthographic {
665 ref mut left,
666 ref mut right,
667 ref mut bottom,
668 ref mut top,
669 ..
670 } = cam.projection
671 {
672 *left = x_min as f32;
673 *right = x_max as f32;
674 *bottom = y_min as f32;
675 *top = y_max as f32;
676 }
677 cam.position.z = 1.0;
678 cam.target.z = 0.0;
679 cam.mark_dirty();
680 self.axes_cameras[idx] = cam;
681 applied = true;
682 }
683 applied
684 }
685
686 pub fn calculate_data_bounds(&mut self) -> Option<(f64, f64, f64, f64)> {
688 let mut min_x = f64::INFINITY;
689 let mut max_x = f64::NEG_INFINITY;
690 let mut min_y = f64::INFINITY;
691 let mut max_y = f64::NEG_INFINITY;
692
693 for node in self.scene.get_visible_nodes() {
694 if let Some(render_data) = &node.render_data {
695 if let Some(bounds) = render_data.bounds {
696 min_x = min_x.min(bounds.min.x as f64);
697 max_x = max_x.max(bounds.max.x as f64);
698 min_y = min_y.min(bounds.min.y as f64);
699 max_y = max_y.max(bounds.max.y as f64);
700 continue;
701 }
702 for vertex in &render_data.vertices {
703 let x = vertex.position[0] as f64;
704 let y = vertex.position[1] as f64;
705 min_x = min_x.min(x);
706 max_x = max_x.max(x);
707 min_y = min_y.min(y);
708 max_y = max_y.max(y);
709 }
710 }
711 }
712
713 if min_x != f64::INFINITY && max_x != f64::NEG_INFINITY {
714 let x_range = (max_x - min_x).max(0.1);
717 let y_range = (max_y - min_y).max(0.1);
718 let x_margin = x_range * 0.04;
719 let y_margin = y_range * 0.04;
720
721 let bounds = (
722 min_x - x_margin,
723 max_x + x_margin,
724 min_y - y_margin,
725 max_y + y_margin,
726 );
727
728 self.data_bounds = Some(bounds);
730 Some(bounds)
731 } else {
732 self.data_bounds = None;
733 None
734 }
735 }
736
737 pub fn fit_camera_to_data(&mut self) -> bool {
741 if self.axes_cameras.len() > 1 {
742 return self.fit_cameras_to_axes_data();
743 }
744
745 if self.axes_has_3d_content(0) {
746 let Some(bounds) = self.axes_bounds(0) else {
747 return false;
748 };
749 let center = (bounds.min + bounds.max) * 0.5;
750 let mut cam = Camera::new();
751 cam.target = center;
752 cam.up = Vec3::Z;
753 cam.position = center + Vec3::new(1.0, -1.0, 1.0);
754 cam.fit_bounds(bounds.min, bounds.max);
755 if let Some(axis_cam) = self.axes_cameras.get_mut(0) {
756 *axis_cam = cam;
757 }
758 return true;
759 }
760
761 if let Some((x_min, x_max, y_min, y_max)) = self.display_bounds_for_axes(0) {
762 let mut cam = Self::create_default_camera();
764 let l = x_min as f32;
765 let r = x_max as f32;
766 let b = y_min as f32;
767 let t = y_max as f32;
768 if let crate::core::camera::ProjectionType::Orthographic {
769 ref mut left,
770 ref mut right,
771 ref mut bottom,
772 ref mut top,
773 ..
774 } = cam.projection
775 {
776 *left = l;
777 *right = r;
778 *bottom = b;
779 *top = t;
780 }
781 cam.position.z = 1.0;
782 cam.target.z = 0.0;
783 cam.mark_dirty();
784
785 if let Some(axis_cam) = self.axes_cameras.get_mut(0) {
786 *axis_cam = cam;
787 }
788 return true;
789 }
790 false
791 }
792
793 pub fn fit_extents(&mut self) {
795 let _ = if self.figure_axes_grid().0 * self.figure_axes_grid().1 > 1 {
796 self.fit_cameras_to_axes_data()
797 } else {
798 self.fit_camera_to_data()
799 };
800 self.clear_all_axes_camera_interaction();
801 self.camera_auto_fit = false;
802 self.needs_update = true;
803 }
804
805 pub fn reset_camera_position(&mut self) {
811 let dir = Vec3::new(1.0, -1.0, 1.0).normalize_or_zero();
812 let data_centers: Vec<Vec3> = (0..self.axes_cameras.len())
813 .map(|idx| {
814 self.axes_bounds(idx)
815 .map(|b| (b.min + b.max) * 0.5)
816 .unwrap_or_else(|| self.axes_cameras[idx].target)
817 })
818 .collect();
819 let display_bounds: Vec<Option<(f64, f64, f64, f64)>> = (0..self.axes_cameras.len())
820 .map(|idx| self.display_bounds_for_axes(idx))
821 .collect();
822 for (idx, c) in self.axes_cameras.iter_mut().enumerate() {
823 if matches!(
824 c.projection,
825 crate::core::camera::ProjectionType::Perspective { .. }
826 ) {
827 let data_center = data_centers.get(idx).copied().unwrap_or(c.target);
828 let dist = (c.position - c.target).length().max(0.1);
829 c.target = data_center;
830 c.up = Vec3::Z;
831 c.position = data_center + dir * dist;
832 c.mark_dirty();
833 } else if let Some((x_min, x_max, y_min, y_max)) = display_bounds[idx] {
834 let mut cam = Self::create_default_camera();
835 if let crate::core::camera::ProjectionType::Orthographic {
836 ref mut left,
837 ref mut right,
838 ref mut bottom,
839 ref mut top,
840 ..
841 } = cam.projection
842 {
843 *left = x_min as f32;
844 *right = x_max as f32;
845 *bottom = y_min as f32;
846 *top = y_max as f32;
847 }
848 cam.position.z = 1.0;
849 cam.target.z = 0.0;
850 cam.mark_dirty();
851 *c = cam;
852 }
853 }
854 self.clear_all_axes_camera_interaction();
855 self.camera_auto_fit = false;
856 self.needs_update = true;
857 }
858
859 pub fn render_to_viewport(
861 &mut self,
862 encoder: &mut wgpu::CommandEncoder,
863 target_view: &wgpu::TextureView,
864 _viewport: (f32, f32, f32, f32), clear_background: bool,
866 background_color: Option<glam::Vec4>,
867 ) -> Result<RenderResult, Box<dyn std::error::Error>> {
868 let start_time = Instant::now();
869
870 let mut render_items = Vec::new();
872 let mut total_vertices = 0;
873 let mut total_triangles = 0;
874
875 for node in self.scene.get_visible_nodes() {
876 if let Some(render_data) = &node.render_data {
877 if let Some(vertex_buffer) = self.wgpu_renderer.vertex_buffer_from_sources(
878 render_data.gpu_vertices.as_ref(),
879 &render_data.vertices,
880 ) {
881 self.wgpu_renderer
882 .ensure_pipeline(render_data.pipeline_type);
883
884 log::trace!(
885 target: "runmat_plot",
886 "upload vertices={}, draw_calls={}",
887 render_data.vertex_count(),
888 render_data.draw_calls.len()
889 );
890
891 render_items.push((render_data, vertex_buffer));
892 total_vertices += render_data.vertex_count();
893
894 if render_data.pipeline_type == crate::core::PipelineType::Triangles {
895 total_triangles += render_data.vertex_count() / 3;
896 }
897 }
898 }
899 }
900
901 let mut cam = self.camera().clone();
903 let view_proj_matrix = cam.view_proj_matrix();
904
905 self.wgpu_renderer
906 .update_uniforms(view_proj_matrix, Mat4::IDENTITY);
907
908 let use_msaa = self.wgpu_renderer.msaa_sample_count > 1;
910 let msaa_view_opt = if use_msaa {
911 let tex = self
912 .wgpu_renderer
913 .device
914 .create_texture(&wgpu::TextureDescriptor {
915 label: Some("runmat_msaa_color_camera"),
916 size: wgpu::Extent3d {
917 width: self.wgpu_renderer.surface_config.width,
918 height: self.wgpu_renderer.surface_config.height,
919 depth_or_array_layers: 1,
920 },
921 mip_level_count: 1,
922 sample_count: self.wgpu_renderer.msaa_sample_count,
923 dimension: wgpu::TextureDimension::D2,
924 format: self.wgpu_renderer.surface_config.format,
925 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
926 view_formats: &[],
927 });
928 Some(tex.create_view(&wgpu::TextureViewDescriptor::default()))
929 } else {
930 None
931 };
932
933 let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
934 label: Some("Viewport Plot Render Pass"),
935 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
936 view: msaa_view_opt.as_ref().unwrap_or(target_view),
937 resolve_target: if use_msaa { Some(target_view) } else { None },
938 ops: wgpu::Operations {
939 load: if clear_background {
940 wgpu::LoadOp::Clear(wgpu::Color {
941 r: background_color.map_or(0.08, |c| c.x as f64),
942 g: background_color.map_or(0.09, |c| c.y as f64),
943 b: background_color.map_or(0.11, |c| c.z as f64),
944 a: background_color.map_or(1.0, |c| c.w as f64),
945 })
946 } else {
947 wgpu::LoadOp::Load
948 },
949 store: wgpu::StoreOp::Store,
950 },
951 })],
952 depth_stencil_attachment: None,
953 occlusion_query_set: None,
954 timestamp_writes: None,
955 });
956
957 let (vx, vy, vw, vh) = _viewport;
959 render_pass.set_viewport(vx, vy, vw, vh, 0.0, 1.0);
960
961 let sw = self.wgpu_renderer.surface_config.width as f32;
963 let sh = self.wgpu_renderer.surface_config.height as f32;
964 let ndc_left = (vx / sw) * 2.0 - 1.0;
965 let ndc_right = ((vx + vw) / sw) * 2.0 - 1.0;
966 let ndc_top = 1.0 - (vy / sh) * 2.0;
967 let ndc_bottom = 1.0 - ((vy + vh) / sh) * 2.0;
968
969 let (x_min, y_min, x_max, y_max) = (0.0_f64, 0.0_f64, 1.0_f64, 1.0_f64);
971 self.wgpu_renderer.update_direct_uniforms(
972 [x_min as f32, y_min as f32],
973 [x_max as f32, y_max as f32],
974 [ndc_left, ndc_bottom],
975 [ndc_right, ndc_top],
976 [sw, sh],
977 );
978
979 drop(render_pass);
981
982 let render_time = start_time.elapsed().as_secs_f64() * 1000.0;
983
984 Ok(RenderResult {
985 success: true,
986 data_bounds: self.data_bounds,
987 vertex_count: total_vertices,
988 triangle_count: total_triangles,
989 render_time_ms: render_time,
990 })
991 }
992
993 pub fn render(
995 &mut self,
996 encoder: &mut wgpu::CommandEncoder,
997 target: RenderTarget<'_>,
998 config: &PlotRenderConfig,
999 ) -> Result<RenderResult, Box<dyn std::error::Error>> {
1000 let start_time = Instant::now();
1001
1002 self.wgpu_renderer.ensure_msaa(config.msaa_samples);
1003
1004 let aspect_ratio = config.width as f32 / config.height as f32;
1006 let mut cam = self.camera().clone();
1007 cam.update_aspect_ratio(aspect_ratio);
1008 let view_proj_matrix = cam.view_proj_matrix();
1009 let model_matrix = Mat4::IDENTITY;
1010 self.wgpu_renderer
1011 .update_uniforms(view_proj_matrix, model_matrix);
1012
1013 let mut render_items = Vec::new();
1015 let mut total_vertices = 0;
1016 let mut total_triangles = 0;
1017
1018 for node in self.scene.get_visible_nodes() {
1019 if let Some(render_data) = &node.render_data {
1020 if let Some((vertex_buffer, index_buffer)) =
1021 self.prepare_buffers_for_render_data(node.id, render_data)
1022 {
1023 self.wgpu_renderer
1024 .ensure_pipeline(render_data.pipeline_type);
1025 render_items.push((render_data, vertex_buffer, index_buffer));
1026
1027 total_vertices += render_data.vertex_count();
1028 if let Some(indices) = &render_data.indices {
1029 total_triangles += indices.len() / 3;
1030 }
1031 }
1032 }
1033 }
1034
1035 let mut image_bind_groups: Vec<Option<wgpu::BindGroup>> =
1037 Vec::with_capacity(render_items.len());
1038 let has_textured_items = render_items.iter().any(|(render_data, _, _)| {
1039 render_data.pipeline_type == crate::core::PipelineType::Textured
1040 });
1041 if has_textured_items {
1042 self.wgpu_renderer.ensure_image_pipeline();
1044 let mut inferred_bounds: Option<(f64, f64, f64, f64)> = None;
1045 for (render_data, _, _) in &render_items {
1046 let Some(bounds) = render_data.bounds.as_ref() else {
1047 continue;
1048 };
1049 let min_x = bounds.min.x as f64;
1050 let max_x = bounds.max.x as f64;
1051 let min_y = bounds.min.y as f64;
1052 let max_y = bounds.max.y as f64;
1053 inferred_bounds = Some(match inferred_bounds {
1054 Some((x0, x1, y0, y1)) => {
1055 (x0.min(min_x), x1.max(max_x), y0.min(min_y), y1.max(max_y))
1056 }
1057 None => (min_x, max_x, min_y, max_y),
1058 });
1059 }
1060
1061 let (mut x_min, mut x_max, mut y_min, mut y_max) = self
1062 .data_bounds
1063 .or(inferred_bounds)
1064 .unwrap_or((-1.0, 1.0, -1.0, 1.0));
1065 if (x_max - x_min).abs() < f64::EPSILON {
1067 x_min -= 0.5;
1068 x_max += 0.5;
1069 }
1070 if (y_max - y_min).abs() < f64::EPSILON {
1071 y_min -= 0.5;
1072 y_max += 0.5;
1073 }
1074 log::trace!(
1075 target: "runmat_plot",
1076 "direct uniforms bounds x=({}, {}) y=({}, {}) size=({}, {})",
1077 x_min,
1078 x_max,
1079 y_min,
1080 y_max,
1081 config.width,
1082 config.height
1083 );
1084 self.wgpu_renderer.update_direct_uniforms(
1085 [x_min as f32, y_min as f32],
1086 [x_max as f32, y_max as f32],
1087 [-1.0, -1.0],
1088 [1.0, 1.0],
1089 [config.width as f32, config.height as f32],
1090 );
1091 }
1092 for (render_data, _vb, _ib) in &render_items {
1093 if render_data.pipeline_type == crate::core::PipelineType::Textured {
1094 if let Some(crate::core::scene::ImageData::Rgba8 {
1095 width,
1096 height,
1097 data,
1098 }) = &render_data.image
1099 {
1100 let (_tex, _view, img_bg) = self
1101 .wgpu_renderer
1102 .create_image_texture_and_bind_group(*width, *height, data);
1103 image_bind_groups.push(Some(img_bg));
1104 } else {
1105 image_bind_groups.push(None);
1106 }
1107 } else {
1108 image_bind_groups.push(None);
1109 }
1110 }
1111 let mut point_style_bind_groups: Vec<Option<wgpu::BindGroup>> =
1112 Vec::with_capacity(render_items.len());
1113 for (render_data, _vb, _ib) in &render_items {
1114 if render_data.pipeline_type == crate::core::PipelineType::Points {
1115 let style = crate::core::renderer::PointStyleUniforms {
1116 face_color: render_data.material.albedo.to_array(),
1117 edge_color: render_data.material.emissive.to_array(),
1118 edge_thickness_px: render_data.material.roughness,
1119 marker_shape: render_data.material.metallic as u32,
1120 _pad: [0.0, 0.0],
1121 };
1122 let (_buf, bg) = self.wgpu_renderer.create_point_style_bind_group(style);
1123 point_style_bind_groups.push(Some(bg));
1124 } else {
1125 point_style_bind_groups.push(None);
1126 }
1127 }
1128
1129 {
1131 let depth_view = self.wgpu_renderer.ensure_depth_view();
1132 let use_msaa = self.wgpu_renderer.msaa_sample_count > 1;
1133 let mut cached_msaa_view: Option<Arc<wgpu::TextureView>> = None;
1134
1135 let (color_view, resolve_target) = if use_msaa {
1136 if let Some(explicit_resolve_target) = target.resolve_target {
1137 (target.view, Some(explicit_resolve_target))
1138 } else {
1139 cached_msaa_view = Some(self.wgpu_renderer.ensure_msaa_color_view());
1140 (
1141 cached_msaa_view
1142 .as_ref()
1143 .expect("msaa color view should exist")
1144 .as_ref(),
1145 Some(target.view),
1146 )
1147 }
1148 } else {
1149 (target.view, target.resolve_target)
1150 };
1151
1152 let depth_clear = match self.wgpu_renderer.depth_mode {
1153 crate::core::DepthMode::Standard => 1.0,
1154 crate::core::DepthMode::ReversedZ => 0.0,
1155 };
1156 let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1157 label: Some("Plot Render Pass"),
1158 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1159 view: color_view,
1160 resolve_target,
1161 ops: wgpu::Operations {
1162 load: wgpu::LoadOp::Clear(wgpu::Color {
1163 r: config.background_color.x as f64,
1164 g: config.background_color.y as f64,
1165 b: config.background_color.z as f64,
1166 a: config.background_color.w as f64,
1167 }),
1168 store: wgpu::StoreOp::Store,
1169 },
1170 })],
1171 depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
1172 view: &depth_view,
1173 depth_ops: Some(wgpu::Operations {
1174 load: wgpu::LoadOp::Clear(depth_clear),
1175 store: wgpu::StoreOp::Discard,
1176 }),
1177 stencil_ops: None,
1178 }),
1179 occlusion_query_set: None,
1180 timestamp_writes: None,
1181 });
1182 let _keep_msaa_view_alive = &cached_msaa_view;
1183
1184 for (i, (render_data, vertex_buffer, index_buffer)) in render_items.iter().enumerate() {
1186 #[cfg(target_arch = "wasm32")]
1187 {
1188 if log::log_enabled!(log::Level::Debug) {
1191 if let Some(v0) = render_data.vertices.first() {
1192 log::debug!(
1193 target: "runmat_plot",
1194 "wasm draw item: pipeline={:?} verts={} v0.pos=({:.3},{:.3},{:.3}) v0.color=({:.3},{:.3},{:.3},{:.3})",
1195 render_data.pipeline_type,
1196 render_data.vertices.len(),
1197 v0.position[0],
1198 v0.position[1],
1199 v0.position[2],
1200 v0.color[0],
1201 v0.color[1],
1202 v0.color[2],
1203 v0.color[3],
1204 );
1205 } else if render_data.gpu_vertices.is_some() {
1206 log::debug!(
1207 target: "runmat_plot",
1208 "wasm draw item: pipeline={:?} using gpu_vertices vertex_count={}",
1209 render_data.pipeline_type,
1210 render_data.vertex_count(),
1211 );
1212 } else {
1213 log::debug!(
1214 target: "runmat_plot",
1215 "wasm draw item: pipeline={:?} has no vertices",
1216 render_data.pipeline_type
1217 );
1218 }
1219 }
1220 }
1221
1222 if render_data.pipeline_type == crate::core::PipelineType::Textured {
1224 let pipeline = self.wgpu_renderer.get_pipeline(render_data.pipeline_type);
1226 render_pass.set_pipeline(pipeline);
1227 render_pass.set_bind_group(
1230 0,
1231 &self.wgpu_renderer.direct_uniform_bind_group,
1232 &[],
1233 );
1234 if let Some(ref img_bg) = image_bind_groups[i] {
1235 render_pass.set_bind_group(1, img_bg, &[]);
1236 }
1237 } else {
1238 let pipeline = self.wgpu_renderer.get_pipeline(render_data.pipeline_type);
1239 render_pass.set_pipeline(pipeline);
1240 render_pass.set_bind_group(0, self.wgpu_renderer.get_uniform_bind_group(), &[]);
1242 }
1243
1244 render_pass.set_vertex_buffer(0, vertex_buffer.slice(..));
1245
1246 if let Some(index_buffer) = index_buffer {
1247 render_pass.set_index_buffer(index_buffer.slice(..), wgpu::IndexFormat::Uint32);
1248 if let Some(indices) = &render_data.indices {
1249 log::trace!(target: "runmat_plot", "draw indexed count={}", indices.len());
1250 render_pass.draw_indexed(0..indices.len() as u32, 0, 0..1);
1251 }
1252 } else {
1253 log::trace!(target: "runmat_plot", "draw direct vertices");
1254 if let Some((args, offset)) = Self::gpu_indirect_args(render_data) {
1255 render_pass.draw_indirect(args, offset);
1256 continue;
1257 }
1258 for draw_call in &render_data.draw_calls {
1260 log::trace!(
1261 target: "runmat_plot",
1262 "draw vertices offset={} count={} instances={}",
1263 draw_call.vertex_offset,
1264 draw_call.vertex_count,
1265 draw_call.instance_count
1266 );
1267 render_pass.draw(
1268 draw_call.vertex_offset as u32
1269 ..(draw_call.vertex_offset + draw_call.vertex_count) as u32,
1270 0..draw_call.instance_count as u32,
1271 );
1272 }
1273 }
1274 }
1275 }
1277
1278 let render_time = start_time.elapsed().as_secs_f64() * 1000.0;
1279
1280 Ok(RenderResult {
1281 success: true,
1282 data_bounds: self.data_bounds,
1283 vertex_count: total_vertices,
1284 triangle_count: total_triangles,
1285 render_time_ms: render_time,
1286 })
1287 }
1288
1289 pub fn render_scene_to_target(
1294 &mut self,
1295 encoder: &mut wgpu::CommandEncoder,
1296 target_view: &wgpu::TextureView,
1297 config: &PlotRenderConfig,
1298 ) -> Result<RenderResult, Box<dyn std::error::Error>> {
1299 let start_time = Instant::now();
1300 let (rows, cols) = self.figure_axes_grid();
1301 let axes_count = rows.saturating_mul(cols);
1302 log::debug!(
1303 "runmat-plot: renderer.scene_to_target.start rows={} cols={} axes_count={} width={} height={}",
1304 rows,
1305 cols,
1306 axes_count,
1307 config.width,
1308 config.height
1309 );
1310 if axes_count <= 1 {
1311 log::debug!("runmat-plot: renderer.scene_to_target.branch_single_axes");
1312 return self.render(
1313 encoder,
1314 RenderTarget {
1315 view: target_view,
1316 resolve_target: None,
1317 },
1318 config,
1319 );
1320 }
1321
1322 let viewports =
1323 Self::compute_tiled_viewports(config.width.max(1), config.height.max(1), rows, cols);
1324 log::debug!(
1325 "runmat-plot: renderer.scene_to_target.branch_subplot_axes viewports={}",
1326 viewports.len()
1327 );
1328 self.render_axes_to_viewports(
1329 encoder,
1330 target_view,
1331 &viewports,
1332 config.msaa_samples.max(1),
1333 config,
1334 )?;
1335 let stats = self.scene.statistics();
1336 Ok(RenderResult {
1337 success: true,
1338 data_bounds: self.data_bounds,
1339 vertex_count: stats.total_vertices,
1340 triangle_count: stats.total_triangles,
1341 render_time_ms: start_time.elapsed().as_secs_f64() * 1000.0,
1342 })
1343 }
1344
1345 fn compute_tiled_viewports(
1346 total_width: u32,
1347 total_height: u32,
1348 rows: usize,
1349 cols: usize,
1350 ) -> Vec<(u32, u32, u32, u32)> {
1351 if rows == 0 || cols == 0 {
1352 return vec![(0, 0, total_width.max(1), total_height.max(1))];
1353 }
1354 let rows_u32 = rows as u32;
1355 let cols_u32 = cols as u32;
1356 let cell_w = (total_width / cols_u32).max(1);
1357 let cell_h = (total_height / rows_u32).max(1);
1358 let mut out = Vec::with_capacity(rows * cols);
1359 for r in 0..rows_u32 {
1360 for c in 0..cols_u32 {
1361 let x = c * cell_w;
1362 let y = r * cell_h;
1363 let mut w = cell_w;
1364 let mut h = cell_h;
1365 if c + 1 == cols_u32 {
1366 w = total_width.saturating_sub(x).max(1);
1367 }
1368 if r + 1 == rows_u32 {
1369 h = total_height.saturating_sub(y).max(1);
1370 }
1371 out.push((x, y, w, h));
1372 }
1373 }
1374 out
1375 }
1376
1377 #[allow(clippy::too_many_arguments)]
1380 pub fn render_camera_to_viewport(
1381 &mut self,
1382 encoder: &mut wgpu::CommandEncoder,
1383 target_view: &wgpu::TextureView,
1384 viewport_scissor: (u32, u32, u32, u32),
1385 config: &PlotRenderConfig,
1386 camera: &Camera,
1387 axes_index: usize,
1388 clear_background: bool,
1389 ) -> Result<RenderResult, Box<dyn std::error::Error>> {
1390 log::debug!(
1391 "runmat-plot: renderer.camera_to_viewport.start axes_index={} viewport=({}, {}, {}, {}) clear_background={}",
1392 axes_index,
1393 viewport_scissor.0,
1394 viewport_scissor.1,
1395 viewport_scissor.2,
1396 viewport_scissor.3,
1397 clear_background
1398 );
1399 let use_msaa = config.msaa_samples.max(1) > 1;
1400 self.wgpu_renderer.ensure_msaa(config.msaa_samples);
1401 let msaa_view_keepalive = if use_msaa {
1402 Some(self.wgpu_renderer.ensure_msaa_color_view())
1403 } else {
1404 None
1405 };
1406 let render_target = if let Some(msaa_view) = msaa_view_keepalive.as_ref() {
1407 RenderTarget {
1408 view: msaa_view.as_ref(),
1409 resolve_target: Some(target_view),
1410 }
1411 } else {
1412 RenderTarget {
1413 view: target_view,
1414 resolve_target: None,
1415 }
1416 };
1417 self.render_camera_to_target_viewport(
1418 encoder,
1419 render_target,
1420 viewport_scissor,
1421 config,
1422 camera,
1423 axes_index,
1424 clear_background,
1425 )
1426 }
1427
1428 #[allow(clippy::too_many_arguments)]
1429 fn render_camera_to_target_viewport(
1430 &mut self,
1431 encoder: &mut wgpu::CommandEncoder,
1432 target: RenderTarget<'_>,
1433 viewport_scissor: (u32, u32, u32, u32),
1434 config: &PlotRenderConfig,
1435 camera: &Camera,
1436 axes_index: usize,
1437 clear_background: bool,
1438 ) -> Result<RenderResult, Box<dyn std::error::Error>> {
1439 let start_time = Instant::now();
1440
1441 self.wgpu_renderer.ensure_msaa(config.msaa_samples);
1443 self.wgpu_renderer.set_depth_mode(config.depth_mode);
1444
1445 let depth_view = self.wgpu_renderer.ensure_depth_view();
1449
1450 let aspect_ratio = (config.width.max(1)) as f32 / (config.height.max(1)) as f32;
1452 let mut cam = camera.clone();
1453 cam.update_aspect_ratio(aspect_ratio);
1454 cam.depth_mode = config.depth_mode;
1455 log::debug!(
1456 "runmat-plot: renderer.camera_to_target_viewport.camera_ready axes_index={} aspect_ratio={} msaa_samples={}",
1457 axes_index,
1458 aspect_ratio,
1459 config.msaa_samples
1460 );
1461
1462 if config.clip_policy.dynamic {
1465 let mut bounds: Option<crate::core::scene::BoundingBox> = None;
1466 for node in self.scene.get_visible_nodes() {
1467 if let Some(rd) = &node.render_data {
1468 if let Some(b) = rd.bounds {
1469 bounds = Some(bounds.map_or(b, |acc| acc.union(&b)));
1470 }
1471 }
1472 }
1473 if let Some(b) = bounds {
1474 cam.update_clip_planes_from_world_aabb(b.min, b.max, &config.clip_policy);
1475 }
1476 }
1477 let view_proj_matrix = cam.view_proj_matrix();
1478 self.wgpu_renderer
1479 .update_uniforms_for_axes(axes_index, view_proj_matrix, Mat4::IDENTITY);
1480 log::debug!(
1481 "runmat-plot: renderer.camera_to_target_viewport.uniforms_updated axes_index={}",
1482 axes_index
1483 );
1484
1485 let (mut sx, mut sy, mut sw, mut sh) = viewport_scissor;
1486 let target_w = self.wgpu_renderer.surface_config.width.max(1);
1487 let target_h = self.wgpu_renderer.surface_config.height.max(1);
1488 if sx >= target_w || sy >= target_h {
1489 return Ok(RenderResult {
1490 success: true,
1491 data_bounds: self.data_bounds,
1492 vertex_count: 0,
1493 triangle_count: 0,
1494 render_time_ms: 0.0,
1495 });
1496 }
1497 sx = sx.min(target_w.saturating_sub(1));
1498 sy = sy.min(target_h.saturating_sub(1));
1499 sw = sw.max(1).min(target_w.saturating_sub(sx).max(1));
1500 sh = sh.max(1).min(target_h.saturating_sub(sy).max(1));
1501 let is_2d = matches!(
1502 cam.projection,
1503 crate::core::camera::ProjectionType::Orthographic { .. }
1504 );
1505 log::debug!(
1506 "runmat-plot: renderer.camera_to_target_viewport.viewport_normalized axes_index={} viewport=({}, {}, {}, {}) is_2d={}",
1507 axes_index,
1508 sx,
1509 sy,
1510 sw,
1511 sh,
1512 is_2d
1513 );
1514 match cam.projection {
1515 crate::core::camera::ProjectionType::Orthographic {
1516 left,
1517 right,
1518 bottom,
1519 top,
1520 ..
1521 } => {
1522 log::debug!(
1523 target: "runmat_plot.draw_camera",
1524 "draw camera axes_index={} is_2d=true viewport=({}, {}, {}, {}) bounds=({}, {})..({}, {}) cfg_wh=({}, {})",
1525 axes_index,
1526 sx,
1527 sy,
1528 sw,
1529 sh,
1530 left,
1531 bottom,
1532 right,
1533 top,
1534 config.width,
1535 config.height
1536 );
1537 }
1538 crate::core::camera::ProjectionType::Perspective { .. } => {
1539 log::debug!(
1540 target: "runmat_plot.draw_camera",
1541 "draw camera axes_index={} is_2d=false viewport=({}, {}, {}, {}) cfg_wh=({}, {})",
1542 axes_index,
1543 sx,
1544 sy,
1545 sw,
1546 sh,
1547 config.width,
1548 config.height
1549 );
1550 }
1551 }
1552
1553 let mut owned_render_data: Vec<Box<crate::core::RenderData>> = Vec::new();
1555 let mut render_items = Vec::new();
1556 let mut grid_plane_buffers: Option<(wgpu::Buffer, wgpu::Buffer)> = None;
1557 let mut total_vertices = 0usize;
1558 let mut total_triangles = 0usize;
1559 log::debug!(
1560 "runmat-plot: renderer.camera_to_target_viewport.collect_render_items.start axes_index={}",
1561 axes_index
1562 );
1563 for node in self.scene.get_visible_nodes() {
1564 if let Some(render_data) = &node.render_data {
1565 if node.axes_index == axes_index {
1566 log::debug!(
1567 target: "runmat_plot.draw_item",
1568 "draw item axes_index={} node_axes_index={} pipeline={:?} vertex_count={} has_indices={} has_bounds={} gpu_vertices={}",
1569 axes_index,
1570 node.axes_index,
1571 render_data.pipeline_type,
1572 render_data.vertex_count(),
1573 render_data.indices.is_some(),
1574 render_data.bounds.is_some(),
1575 render_data.gpu_vertices.is_some()
1576 );
1577 }
1578 if let Some((vb, ib)) = self.prepare_buffers_for_render_data(node.id, render_data) {
1579 self.wgpu_renderer
1580 .ensure_pipeline(render_data.pipeline_type);
1581 total_vertices += render_data.vertex_count();
1582 if let Some(indices) = &render_data.indices {
1583 total_triangles += indices.len() / 3;
1584 }
1585 render_items.push((render_data, vb, ib));
1586 }
1587 }
1588 }
1589 log::debug!(
1590 "runmat-plot: renderer.camera_to_target_viewport.collect_render_items.ok axes_index={} items={} total_vertices={} total_triangles={}",
1591 axes_index,
1592 render_items.len(),
1593 total_vertices,
1594 total_triangles
1595 );
1596
1597 if !is_2d {
1600 let view_proj = view_proj_matrix;
1601 let inv_view_proj = view_proj.inverse();
1602
1603 let unproject = |ndc_x: f32, ndc_y: f32, ndc_z: f32| -> Option<Vec3> {
1604 let clip = Vec4::new(ndc_x, ndc_y, ndc_z, 1.0);
1605 let world = inv_view_proj * clip;
1606 if !world.w.is_finite() || world.w.abs() < 1e-6 {
1607 return None;
1608 }
1609 let p = world.truncate() / world.w;
1610 if p.x.is_finite() && p.y.is_finite() && p.z.is_finite() {
1611 Some(p)
1612 } else {
1613 None
1614 }
1615 };
1616
1617 let ray_intersect_z0 = |ndc_x: f32, ndc_y: f32| -> Option<Vec3> {
1618 let p0 = unproject(ndc_x, ndc_y, -1.0)?;
1620 let p1 = unproject(ndc_x, ndc_y, 1.0)?;
1621 let dir = p1 - p0;
1622 if !dir.z.is_finite() || dir.z.abs() < 1e-8 {
1623 return None;
1624 }
1625 let t = (-p0.z) / dir.z;
1626 if !t.is_finite() || t <= 0.0 {
1627 return None;
1628 }
1629 Some(p0 + dir * t)
1630 };
1631
1632 let mut plane_pts: Vec<Vec3> = Vec::new();
1633 for (nx, ny) in [(-1.0, -1.0), (1.0, -1.0), (1.0, 1.0), (-1.0, 1.0)] {
1634 if let Some(p) = ray_intersect_z0(nx, ny) {
1635 plane_pts.push(p);
1636 }
1637 }
1638
1639 let mut min_x = 0.0_f32;
1641 let mut max_x = 1.0_f32;
1642 let mut min_y = 0.0_f32;
1643 let mut max_y = 1.0_f32;
1644
1645 if plane_pts.len() >= 2 {
1646 min_x = plane_pts.iter().map(|p| p.x).fold(f32::INFINITY, f32::min);
1647 max_x = plane_pts
1648 .iter()
1649 .map(|p| p.x)
1650 .fold(f32::NEG_INFINITY, f32::max);
1651 min_y = plane_pts.iter().map(|p| p.y).fold(f32::INFINITY, f32::min);
1652 max_y = plane_pts
1653 .iter()
1654 .map(|p| p.y)
1655 .fold(f32::NEG_INFINITY, f32::max);
1656 } else if let crate::core::camera::ProjectionType::Perspective { fov, .. } =
1657 cam.projection
1658 {
1659 let dist = (cam.position - cam.target).length().max(1e-3);
1660 let extent = (dist * (0.5 * fov).tan() * 1.25).max(0.5);
1661 let center = Vec3::new(cam.target.x, cam.target.y, 0.0);
1662 min_x = center.x - extent;
1663 max_x = center.x + extent;
1664 min_y = center.y - extent;
1665 max_y = center.y + extent;
1666 }
1667
1668 let dx = (max_x - min_x).abs().max(1e-3);
1670 let dy = (max_y - min_y).abs().max(1e-3);
1671 let margin_x = dx * 0.04;
1672 let margin_y = dy * 0.04;
1673 min_x -= margin_x;
1674 max_x += margin_x;
1675 min_y -= margin_y;
1676 max_y += margin_y;
1677
1678 let project_to_px = |p: Vec3| -> Option<(f32, f32)> {
1679 let clip = view_proj * Vec4::new(p.x, p.y, p.z, 1.0);
1680 if !clip.w.is_finite() || clip.w.abs() < 1e-6 {
1681 return None;
1682 }
1683 let ndc = clip.truncate() / clip.w;
1684 if !(ndc.x.is_finite() && ndc.y.is_finite()) {
1685 return None;
1686 }
1687 let px = ((ndc.x + 1.0) * 0.5) * (sw.max(1) as f32);
1688 let py = ((1.0 - ndc.y) * 0.5) * (sh.max(1) as f32);
1689 Some((px, py))
1690 };
1691
1692 let nice_step = |raw: f64| -> f64 {
1693 if !raw.is_finite() || raw <= 0.0 {
1694 return 1.0;
1695 }
1696 let pow10 = 10.0_f64.powf(raw.log10().floor());
1697 let norm = raw / pow10;
1698 let mult = if norm <= 1.0 {
1699 1.0
1700 } else if norm <= 2.0 {
1701 2.0
1702 } else if norm <= 5.0 {
1703 5.0
1704 } else {
1705 10.0
1706 };
1707 mult * pow10
1708 };
1709
1710 let cx = (min_x + max_x) * 0.5;
1712 let cy = (min_y + max_y) * 0.5;
1713 let center = Vec3::new(cx, cy, 0.0);
1714 let px_per_world = {
1715 let a = project_to_px(center);
1716 let b = project_to_px(center + Vec3::new(1.0, 0.0, 0.0));
1717 match (a, b) {
1718 (Some((ax, ay)), Some((bx, by))) => ((bx - ax).hypot(by - ay)).max(1e-3),
1719 _ => 1.0,
1720 }
1721 };
1722 let desired_major_px = 120.0_f64;
1723 let major_step = nice_step((desired_major_px / (px_per_world as f64)).max(1e-6));
1724 let mut minor_step = major_step / 10.0;
1725 if !minor_step.is_finite() || minor_step <= 0.0 {
1726 minor_step = major_step.max(1.0);
1727 }
1728
1729 let max_minor_lines = 180.0;
1731 let minor_count_x = (dx as f64 / minor_step).abs();
1732 let minor_count_y = (dy as f64 / minor_step).abs();
1733 if minor_count_x > max_minor_lines || minor_count_y > max_minor_lines {
1734 minor_step = (major_step / 5.0).max(major_step); }
1736
1737 let mut helper_vertices: Vec<Vertex> = Vec::new();
1738 let mut push_line = |a: Vec3, b: Vec3, color: Vec4| {
1739 helper_vertices.push(Vertex::new(a, color));
1740 helper_vertices.push(Vertex::new(b, color));
1741 };
1742
1743 let z_grid = -1e-4_f32;
1745
1746 if self.overlay_show_grid_for_axes(axes_index) {
1749 let theme = self.theme.build_theme();
1750 let bg = theme.get_background_color();
1751 let grid = theme.get_grid_color();
1752 let bg_luma = 0.2126 * bg.x + 0.7152 * bg.y + 0.0722 * bg.z;
1753 let mut major_rgb = [grid.x, grid.y, grid.z];
1754 let mut minor_rgb = [grid.x, grid.y, grid.z];
1755 let mut major_alpha = grid.w.clamp(0.08, 0.22);
1756 let mut minor_alpha = (grid.w * 0.45).clamp(0.04, 0.14);
1757 if bg_luma <= 0.62 {
1758 major_rgb = [grid.x * 0.80, grid.y * 0.80, grid.z * 0.80];
1759 minor_rgb = [grid.x * 0.68, grid.y * 0.68, grid.z * 0.68];
1760 }
1761 if bg_luma > 0.62 {
1762 major_rgb = [grid.x * 0.45, grid.y * 0.45, grid.z * 0.45];
1763 minor_rgb = [grid.x * 0.33, grid.y * 0.33, grid.z * 0.33];
1764 major_alpha = major_alpha.max(0.24);
1765 minor_alpha = minor_alpha.max(0.12);
1766 }
1767 self.wgpu_renderer.ensure_grid_plane_pipeline();
1768 self.wgpu_renderer.update_grid_uniforms_for_axes(
1769 axes_index,
1770 crate::core::renderer::GridUniforms {
1771 major_step: major_step as f32,
1772 minor_step: minor_step as f32,
1773 fade_start: (0.60 * dx.max(dy)).max(major_step as f32),
1774 fade_end: (0.95 * dx.max(dy)).max((major_step as f32) * 2.0),
1775 camera_pos: cam.position.to_array(),
1776 _pad0: 0.0,
1777 target_pos: Vec3::new(cam.target.x, cam.target.y, 0.0).to_array(),
1778 _pad1: 0.0,
1779 major_color: [major_rgb[0], major_rgb[1], major_rgb[2], major_alpha],
1780 minor_color: [minor_rgb[0], minor_rgb[1], minor_rgb[2], minor_alpha],
1781 },
1782 );
1783
1784 let quad_vertices = [
1785 Vertex::new(Vec3::new(min_x, min_y, z_grid), Vec4::ONE),
1786 Vertex::new(Vec3::new(max_x, min_y, z_grid), Vec4::ONE),
1787 Vertex::new(Vec3::new(max_x, max_y, z_grid), Vec4::ONE),
1788 Vertex::new(Vec3::new(min_x, max_y, z_grid), Vec4::ONE),
1789 ];
1790 let quad_indices: [u32; 6] = [0, 1, 2, 0, 2, 3];
1791 let vb = self.wgpu_renderer.create_vertex_buffer(&quad_vertices);
1792 let ib = self.wgpu_renderer.create_index_buffer(&quad_indices);
1793 grid_plane_buffers = Some((vb, ib));
1794 }
1795
1796 let axis_len = (major_step as f32 * 5.0).clamp(0.5, (dx.max(dy) * 0.6).max(0.5));
1798 let origin = Vec3::new(0.0, 0.0, 0.0);
1799 let col_x = Vec4::new(0.92, 0.25, 0.25, 0.85);
1800 let col_y = Vec4::new(0.35, 0.90, 0.45, 0.85);
1801 let col_z = Vec4::new(0.35, 0.62, 0.98, 0.85);
1802 push_line(origin, origin + Vec3::new(axis_len, 0.0, 0.0), col_x);
1803 push_line(origin, origin + Vec3::new(0.0, axis_len, 0.0), col_y);
1804 push_line(origin, origin + Vec3::new(0.0, 0.0, axis_len), col_z);
1805
1806 let tick_max = (major_step as f32 * 0.25).max(1.0e-6);
1812 let tick_min = 0.01_f32.min(tick_max);
1813 let tick_len = (axis_len * 0.04).clamp(tick_min, tick_max);
1814 let max_ticks = 6usize;
1815 let mut add_ticks = |axis: Vec3, perp: Vec3, col: Vec4| {
1816 if major_step <= 0.0 {
1817 return;
1818 }
1819 for i in 1..=max_ticks {
1820 let t = (i as f32) * (major_step as f32);
1821 if t >= axis_len * 0.999 {
1822 break;
1823 }
1824 let p = origin + axis * t;
1825 push_line(
1826 p - perp * tick_len,
1827 p + perp * tick_len,
1828 Vec4::new(col.x, col.y, col.z, col.w * 0.85),
1829 );
1830 }
1831 };
1832 add_ticks(Vec3::X, Vec3::Y, col_x);
1833 add_ticks(Vec3::Y, Vec3::X, col_y);
1834 add_ticks(Vec3::Z, Vec3::X, col_z);
1835
1836 if !helper_vertices.is_empty() {
1837 let rd = Box::new(crate::core::RenderData {
1838 pipeline_type: crate::core::PipelineType::Lines,
1839 vertices: helper_vertices,
1840 indices: None,
1841 gpu_vertices: None,
1842 bounds: None,
1843 material: crate::core::Material::default(),
1844 draw_calls: vec![crate::core::DrawCall {
1845 vertex_offset: 0,
1846 vertex_count: 0, index_offset: None,
1848 index_count: None,
1849 instance_count: 1,
1850 }],
1851 image: None,
1852 });
1853 owned_render_data.push(rd);
1854 let idx = owned_render_data.len() - 1;
1855 let vcount = owned_render_data[idx].vertices.len();
1857 if let Some(dc) = owned_render_data[idx].draw_calls.get_mut(0) {
1858 dc.vertex_count = vcount;
1859 }
1860 let vb = Arc::new(
1861 self.wgpu_renderer
1862 .create_vertex_buffer(&owned_render_data[idx].vertices),
1863 );
1864 let rd_ref: &crate::core::RenderData = &owned_render_data[idx];
1866 render_items.insert(0, (rd_ref, vb, None));
1867 total_vertices += vcount;
1868 }
1869 }
1870
1871 let mut point_buffers: Vec<Option<(wgpu::Buffer, usize)>> =
1873 Vec::with_capacity(render_items.len());
1874 for (render_data, _vb, _ib) in render_items.iter() {
1875 if matches!(render_data.pipeline_type, crate::core::PipelineType::Points) {
1876 let expanded = self
1877 .wgpu_renderer
1878 .create_direct_point_vertices(&render_data.vertices, 0.0);
1880 let buf = self.wgpu_renderer.create_vertex_buffer(&expanded);
1881 point_buffers.push(Some((buf, expanded.len())));
1882 } else {
1883 point_buffers.push(None);
1884 }
1885 }
1886 let has_textured_items = render_items.iter().any(|(render_data, _vb, _ib)| {
1888 render_data.pipeline_type == crate::core::PipelineType::Textured
1889 });
1890 if has_textured_items {
1891 self.wgpu_renderer.ensure_image_pipeline();
1892 }
1893 let mut image_bind_groups: Vec<Option<wgpu::BindGroup>> =
1894 Vec::with_capacity(render_items.len());
1895
1896 for (render_data, _vb, _ib) in render_items.iter() {
1897 if render_data.pipeline_type == crate::core::PipelineType::Textured {
1898 if let Some(crate::core::scene::ImageData::Rgba8 {
1899 width,
1900 height,
1901 data,
1902 }) = &render_data.image
1903 {
1904 let (_t, _v, bg) = self
1905 .wgpu_renderer
1906 .create_image_texture_and_bind_group(*width, *height, data);
1907 image_bind_groups.push(Some(bg));
1908 } else {
1909 image_bind_groups.push(None);
1910 }
1911 } else {
1912 image_bind_groups.push(None);
1913 }
1914 }
1915 let mut point_style_bind_groups: Vec<Option<wgpu::BindGroup>> =
1917 Vec::with_capacity(render_items.len());
1918 for (render_data, _vb, _ib) in render_items.iter() {
1919 if matches!(render_data.pipeline_type, crate::core::PipelineType::Points) {
1920 let style = crate::core::renderer::PointStyleUniforms {
1921 face_color: render_data.material.albedo.to_array(),
1922 edge_color: render_data.material.emissive.to_array(),
1923 edge_thickness_px: render_data.material.roughness,
1924 marker_shape: render_data.material.metallic as u32,
1925 _pad: [0.0, 0.0],
1926 };
1927 let (_buf, bg) = self.wgpu_renderer.create_point_style_bind_group(style);
1928 point_style_bind_groups.push(Some(bg));
1929 } else {
1930 point_style_bind_groups.push(None);
1931 }
1932 }
1933
1934 let mut grid_vb_opt: Option<wgpu::Buffer> = None;
1937 if is_2d && self.overlay_show_grid_for_axes(axes_index) {
1938 if let Some((l, r, b, t)) = self.view_bounds_for_axes(axes_index) {
1939 self.wgpu_renderer.update_direct_uniforms_for_axes(
1941 axes_index,
1942 [l as f32, b as f32],
1943 [r as f32, t as f32],
1944 [-1.0, -1.0],
1945 [1.0, 1.0],
1946 [sw.max(1) as f32, sh.max(1) as f32],
1947 );
1948 self.wgpu_renderer.ensure_direct_line_pipeline();
1949
1950 let x_range = (r - l).max(1e-6);
1951 let y_range = (t - b).max(1e-6);
1952 let x_step = plot_utils::calculate_tick_interval(x_range);
1953 let y_step = plot_utils::calculate_tick_interval(y_range);
1954 let mut grid_vertices: Vec<Vertex> = Vec::new();
1955 let g = 80.0_f32 / 255.0_f32;
1956 let col = Vec4::new(g, g, g, 1.0);
1957 if x_step.is_finite() && x_step > 0.0 {
1958 let mut x = ((l / x_step).ceil() * x_step) as f32;
1959 let b_f = b as f32;
1960 let t_f = t as f32;
1961 while (x as f64) <= r {
1962 grid_vertices.push(Vertex::new(Vec3::new(x, b_f, 0.0), col));
1963 grid_vertices.push(Vertex::new(Vec3::new(x, t_f, 0.0), col));
1964 x += x_step as f32;
1965 }
1966 }
1967 if y_step.is_finite() && y_step > 0.0 {
1968 let mut y = ((b / y_step).ceil() * y_step) as f32;
1969 let l_f = l as f32;
1970 let r_f = r as f32;
1971 while (y as f64) <= t {
1972 grid_vertices.push(Vertex::new(Vec3::new(l_f, y, 0.0), col));
1973 grid_vertices.push(Vertex::new(Vec3::new(r_f, y, 0.0), col));
1974 y += y_step as f32;
1975 }
1976 }
1977 if !grid_vertices.is_empty() {
1978 grid_vb_opt = Some(self.wgpu_renderer.create_vertex_buffer(&grid_vertices));
1979 }
1980 }
1981 }
1982
1983 let bounds_opt = if is_2d {
1985 match cam.projection {
1986 crate::core::camera::ProjectionType::Orthographic {
1987 left,
1988 right,
1989 bottom,
1990 top,
1991 ..
1992 } => Some((left as f64, right as f64, bottom as f64, top as f64)),
1993 _ => self.data_bounds,
1994 }
1995 } else {
1996 None
1997 };
1998 if is_2d {
1999 if let Some((l, r, b, t)) = bounds_opt {
2000 self.wgpu_renderer.update_direct_uniforms_for_axes(
2001 axes_index,
2002 [l as f32, b as f32],
2003 [r as f32, t as f32],
2004 [-1.0, -1.0],
2005 [1.0, 1.0],
2006 [sw.max(1) as f32, sh.max(1) as f32],
2007 );
2008 }
2009 self.wgpu_renderer.ensure_direct_triangle_pipeline();
2010 self.wgpu_renderer.ensure_direct_line_pipeline();
2011 self.wgpu_renderer.ensure_direct_point_pipeline();
2012 } else {
2013 self.wgpu_renderer
2015 .ensure_pipeline(crate::core::PipelineType::Triangles);
2016 self.wgpu_renderer
2017 .ensure_pipeline(crate::core::PipelineType::Lines);
2018 self.wgpu_renderer
2019 .ensure_pipeline(crate::core::PipelineType::Points);
2020 }
2021
2022 {
2024 let use_msaa = self.wgpu_renderer.msaa_sample_count > 1;
2026 log::debug!(
2027 "runmat-plot: renderer.camera_to_target_viewport.render_pass_start axes_index={} use_msaa={} clear_background={}",
2028 axes_index,
2029 use_msaa,
2030 clear_background
2031 );
2032
2033 let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
2034 label: Some("Plot Camera Viewport Pass"),
2035 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
2036 view: target.view,
2037 resolve_target: if use_msaa {
2038 target.resolve_target
2039 } else {
2040 None
2041 },
2042 ops: wgpu::Operations {
2043 load: if clear_background {
2044 wgpu::LoadOp::Clear(wgpu::Color {
2045 r: config.background_color.x as f64,
2046 g: config.background_color.y as f64,
2047 b: config.background_color.z as f64,
2048 a: config.background_color.w as f64,
2049 })
2050 } else {
2051 wgpu::LoadOp::Load
2052 },
2053 store: wgpu::StoreOp::Store,
2054 },
2055 })],
2056 depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
2057 view: depth_view.as_ref(),
2058 depth_ops: Some(wgpu::Operations {
2059 load: wgpu::LoadOp::Clear(match config.depth_mode {
2060 DepthMode::Standard => 1.0,
2061 DepthMode::ReversedZ => 0.0,
2062 }),
2063 store: wgpu::StoreOp::Store,
2064 }),
2065 stencil_ops: None,
2066 }),
2067 timestamp_writes: None,
2068 occlusion_query_set: None,
2069 });
2070
2071 render_pass.set_viewport(
2073 sx as f32,
2074 sy as f32,
2075 sw.max(1) as f32,
2076 sh.max(1) as f32,
2077 0.0,
2078 1.0,
2079 );
2080 render_pass.set_scissor_rect(sx, sy, sw.max(1), sh.max(1));
2081 log::debug!(
2082 "runmat-plot: renderer.camera_to_target_viewport.render_pass_ready axes_index={} viewport=({}, {}, {}, {})",
2083 axes_index,
2084 sx,
2085 sy,
2086 sw.max(1),
2087 sh.max(1)
2088 );
2089 if let Some(ref vb_grid) = grid_vb_opt {
2090 if let Some(ref pipeline) = self.wgpu_renderer.direct_line_pipeline {
2091 log::debug!(
2092 "runmat-plot: renderer.camera_to_target_viewport.draw_grid_start axes_index={} vertex_buffer_size={}",
2093 axes_index,
2094 vb_grid.size()
2095 );
2096 render_pass.set_pipeline(pipeline);
2097 render_pass.set_bind_group(
2098 0,
2099 self.wgpu_renderer
2100 .get_direct_uniform_bind_group_for_axes(axes_index),
2101 &[],
2102 );
2103 render_pass.set_vertex_buffer(0, vb_grid.slice(..));
2104 render_pass.draw(
2109 0..(vb_grid.size() / std::mem::size_of::<Vertex>() as u64) as u32,
2110 0..1,
2111 );
2112 log::debug!(
2113 "runmat-plot: renderer.camera_to_target_viewport.draw_grid_ok axes_index={}",
2114 axes_index
2115 );
2116 }
2117 }
2118
2119 let use_direct_for_triangles = is_2d;
2121 let use_direct_for_lines = is_2d;
2122 let direct_tri_pipeline = if use_direct_for_triangles && bounds_opt.is_some() {
2123 self.wgpu_renderer
2124 .direct_triangle_pipeline
2125 .as_ref()
2126 .map(|p| p as *const wgpu::RenderPipeline)
2127 } else {
2128 None
2129 };
2130 let direct_line_pipeline = if use_direct_for_lines && bounds_opt.is_some() {
2131 self.wgpu_renderer
2132 .direct_line_pipeline
2133 .as_ref()
2134 .map(|p| p as *const wgpu::RenderPipeline)
2135 } else {
2136 None
2137 };
2138 let direct_point_pipeline = if is_2d && bounds_opt.is_some() {
2139 self.wgpu_renderer
2140 .direct_point_pipeline
2141 .as_ref()
2142 .map(|p| p as *const wgpu::RenderPipeline)
2143 } else {
2144 None
2145 };
2146
2147 let mut __temp_point_buffers_cam: Vec<wgpu::Buffer> = Vec::new();
2149 for (idx, (render_data, vertex_buffer, index_buffer)) in render_items.iter().enumerate()
2150 {
2151 let is_triangles = matches!(
2152 render_data.pipeline_type,
2153 crate::core::PipelineType::Triangles
2154 );
2155 let is_lines =
2156 matches!(render_data.pipeline_type, crate::core::PipelineType::Lines);
2157 let is_points =
2158 matches!(render_data.pipeline_type, crate::core::PipelineType::Points);
2159 let is_textured = matches!(
2160 render_data.pipeline_type,
2161 crate::core::PipelineType::Textured
2162 );
2163 let use_direct = is_2d
2165 && ((use_direct_for_triangles && is_triangles)
2166 || (use_direct_for_lines && is_lines)
2167 || is_points)
2168 && bounds_opt.is_some();
2169 log::debug!(
2170 "runmat-plot: renderer.camera_to_target_viewport.draw_item_start axes_index={} item_index={} pipeline={:?} use_direct={} textured={} indexed={} draw_calls={} point_buffer={} ",
2171 axes_index,
2172 idx,
2173 render_data.pipeline_type,
2174 use_direct,
2175 is_textured,
2176 index_buffer.is_some(),
2177 render_data.draw_calls.len(),
2178 point_buffers[idx].is_some()
2179 );
2180
2181 if use_direct {
2182 let pipeline_ref: &wgpu::RenderPipeline = unsafe {
2184 if is_triangles {
2185 direct_tri_pipeline.unwrap().as_ref().unwrap()
2186 } else if is_lines {
2187 direct_line_pipeline.unwrap().as_ref().unwrap()
2188 } else {
2189 direct_point_pipeline.unwrap().as_ref().unwrap()
2190 }
2191 };
2192 let uniform_bg = self
2193 .wgpu_renderer
2194 .get_direct_uniform_bind_group_for_axes(axes_index);
2195 render_pass.set_pipeline(pipeline_ref);
2196 render_pass.set_bind_group(0, uniform_bg, &[]);
2197 log::debug!(
2198 "runmat-plot: renderer.camera_to_target_viewport.draw_item_pipeline_ready axes_index={} item_index={} branch=direct",
2199 axes_index,
2200 idx
2201 );
2202 } else if is_textured {
2203 let pipeline = self
2204 .wgpu_renderer
2205 .get_pipeline(crate::core::PipelineType::Textured);
2206 render_pass.set_pipeline(pipeline);
2207 render_pass.set_bind_group(
2208 0,
2209 self.wgpu_renderer
2210 .get_direct_uniform_bind_group_for_axes(axes_index),
2211 &[],
2212 );
2213 if let Some(ref bg) = image_bind_groups[idx] {
2214 render_pass.set_bind_group(1, bg, &[]);
2215 }
2216 log::debug!(
2217 "runmat-plot: renderer.camera_to_target_viewport.draw_item_pipeline_ready axes_index={} item_index={} branch=textured",
2218 axes_index,
2219 idx
2220 );
2221 } else {
2222 let pipeline = self.wgpu_renderer.get_pipeline(render_data.pipeline_type);
2223 render_pass.set_pipeline(pipeline);
2224 render_pass.set_bind_group(
2225 0,
2226 self.wgpu_renderer
2227 .get_uniform_bind_group_for_axes(axes_index),
2228 &[],
2229 );
2230 log::debug!(
2231 "runmat-plot: renderer.camera_to_target_viewport.draw_item_pipeline_ready axes_index={} item_index={} branch=standard",
2232 axes_index,
2233 idx
2234 );
2235 }
2236
2237 if is_points && use_direct {
2238 if let Some((ref buf, len)) = point_buffers[idx] {
2239 if let Some(ref bg) = point_style_bind_groups[idx] {
2240 render_pass.set_bind_group(1, bg, &[]);
2241 }
2242 render_pass.set_vertex_buffer(0, buf.slice(..));
2243 render_pass.draw(0..len as u32, 0..1);
2244 log::debug!(
2245 "runmat-plot: renderer.camera_to_target_viewport.draw_item_ok axes_index={} item_index={} mode=direct_points vertices={}",
2246 axes_index,
2247 idx,
2248 len
2249 );
2250 continue;
2251 }
2252 } else {
2253 render_pass.set_vertex_buffer(0, vertex_buffer.slice(..));
2254 }
2255 if let Some(index_buffer_ref) = index_buffer {
2256 render_pass
2257 .set_index_buffer(index_buffer_ref.slice(..), wgpu::IndexFormat::Uint32);
2258 if let Some(indices) = &render_data.indices {
2259 render_pass.draw_indexed(0..indices.len() as u32, 0, 0..1);
2260 log::debug!(
2261 "runmat-plot: renderer.camera_to_target_viewport.draw_item_ok axes_index={} item_index={} mode=indexed indices={}",
2262 axes_index,
2263 idx,
2264 indices.len()
2265 );
2266 }
2267 } else {
2268 for dc in &render_data.draw_calls {
2269 render_pass.draw(
2270 dc.vertex_offset as u32..(dc.vertex_offset + dc.vertex_count) as u32,
2271 0..dc.instance_count as u32,
2272 );
2273 log::debug!(
2274 "runmat-plot: renderer.camera_to_target_viewport.draw_call_ok axes_index={} item_index={} mode=draw vertex_offset={} vertex_count={} instances={}",
2275 axes_index,
2276 idx,
2277 dc.vertex_offset,
2278 dc.vertex_count,
2279 dc.instance_count
2280 );
2281 }
2282 }
2283 }
2284
2285 if let Some((ref vb, ref ib)) = grid_plane_buffers {
2287 if let Some(pipeline) = self.wgpu_renderer.grid_plane_pipeline() {
2288 log::debug!(
2289 "runmat-plot: renderer.camera_to_target_viewport.draw_grid_plane_start axes_index={}",
2290 axes_index
2291 );
2292 render_pass.set_pipeline(pipeline);
2293 render_pass.set_bind_group(
2294 0,
2295 self.wgpu_renderer
2296 .get_uniform_bind_group_for_axes(axes_index),
2297 &[],
2298 );
2299 render_pass.set_bind_group(
2300 1,
2301 self.wgpu_renderer
2302 .get_grid_uniform_bind_group_for_axes(axes_index),
2303 &[],
2304 );
2305 render_pass.set_vertex_buffer(0, vb.slice(..));
2306 render_pass.set_index_buffer(ib.slice(..), wgpu::IndexFormat::Uint32);
2307 render_pass.draw_indexed(0..6, 0, 0..1);
2308 log::debug!(
2309 "runmat-plot: renderer.camera_to_target_viewport.draw_grid_plane_ok axes_index={}",
2310 axes_index
2311 );
2312 }
2313 }
2314 }
2315
2316 log::debug!(
2317 "runmat-plot: renderer.camera_to_target_viewport.ok axes_index={} total_vertices={} total_triangles={}",
2318 axes_index,
2319 total_vertices,
2320 total_triangles
2321 );
2322
2323 Ok(RenderResult {
2324 success: true,
2325 data_bounds: self.data_bounds,
2326 vertex_count: total_vertices,
2327 triangle_count: total_triangles,
2328 render_time_ms: start_time.elapsed().as_secs_f64() * 1000.0,
2329 })
2330 }
2331
2332 pub fn render_axes_to_viewports(
2335 &mut self,
2336 encoder: &mut wgpu::CommandEncoder,
2337 target_view: &wgpu::TextureView,
2338 axes_viewports: &[(u32, u32, u32, u32)],
2339 msaa_samples: u32,
2340 base_config: &PlotRenderConfig,
2341 ) -> Result<(), Box<dyn std::error::Error>> {
2342 log::debug!(
2343 "runmat-plot: renderer.axes_to_viewports.start viewport_count={} msaa_samples={} width={} height={}",
2344 axes_viewports.len(),
2345 msaa_samples,
2346 base_config.width,
2347 base_config.height
2348 );
2349 let mut axes_to_nodes: std::collections::HashMap<usize, Vec<crate::core::scene::NodeId>> =
2351 std::collections::HashMap::new();
2352 for node in self.scene.get_visible_nodes() {
2353 axes_to_nodes
2354 .entry(node.axes_index)
2355 .or_default()
2356 .push(node.id);
2357 }
2358
2359 if self.axes_cameras.is_empty() {
2360 self.axes_cameras.push(Self::create_default_camera());
2361 }
2362 self.wgpu_renderer
2363 .ensure_axes_uniform_capacity(axes_viewports.len().max(1));
2364
2365 let all_ids: Vec<crate::core::scene::NodeId> = self
2367 .scene
2368 .get_visible_nodes()
2369 .into_iter()
2370 .map(|n| n.id)
2371 .collect();
2372 let active_axes: Vec<usize> = axes_viewports
2373 .iter()
2374 .enumerate()
2375 .filter_map(|(ax_idx, _)| {
2376 axes_to_nodes
2377 .get(&ax_idx)
2378 .filter(|ids| !ids.is_empty())
2379 .map(|_| ax_idx)
2380 })
2381 .collect();
2382 if active_axes.is_empty() {
2383 log::debug!("runmat-plot: renderer.axes_to_viewports.no_active_axes");
2384 return Ok(());
2385 }
2386
2387 self.wgpu_renderer.ensure_msaa(msaa_samples.max(1));
2388 let shared_msaa_view = if self.wgpu_renderer.msaa_sample_count > 1 {
2389 Some(self.wgpu_renderer.ensure_msaa_color_view())
2390 } else {
2391 None
2392 };
2393
2394 for (ax_idx, viewport) in axes_viewports.iter().enumerate() {
2395 log::debug!(
2396 "runmat-plot: renderer.axes_to_viewports.viewport axes_index={} viewport=({}, {}, {}, {})",
2397 ax_idx,
2398 viewport.0,
2399 viewport.1,
2400 viewport.2,
2401 viewport.3
2402 );
2403 let ids_for_axes = axes_to_nodes.get(&ax_idx).cloned().unwrap_or_default();
2404 if ids_for_axes.is_empty() {
2405 log::debug!(
2406 "runmat-plot: renderer.axes_to_viewports.skip_empty_axes axes_index={}",
2407 ax_idx
2408 );
2409 continue;
2410 }
2411
2412 let mut hidden_ids: Vec<crate::core::scene::NodeId> = Vec::new();
2414 for id in &all_ids {
2415 if !ids_for_axes.contains(id) {
2416 if let Some(node) = self.scene.get_node_mut(*id) {
2417 if node.visible {
2418 node.visible = false;
2419 hidden_ids.push(*id);
2420 }
2421 }
2422 }
2423 }
2424 let cam = self
2426 .axes_cameras
2427 .get(ax_idx)
2428 .cloned()
2429 .unwrap_or_else(Self::create_default_camera);
2430 let _ = self.calculate_data_bounds();
2431
2432 let mut cfg = base_config.clone();
2434 cfg.width = viewport.2;
2435 cfg.height = viewport.3;
2436 cfg.msaa_samples = msaa_samples.max(1);
2437 let is_first_axes = Some(&ax_idx) == active_axes.first();
2438 let is_last_axes = Some(&ax_idx) == active_axes.last();
2439 log::debug!(
2440 "runmat-plot: renderer.axes_to_viewports.axes_ready axes_index={} node_count={} first_axes={} last_axes={}",
2441 ax_idx,
2442 ids_for_axes.len(),
2443 is_first_axes,
2444 is_last_axes
2445 );
2446 let render_target = if let Some(ref msaa_view) = shared_msaa_view {
2447 RenderTarget {
2448 view: msaa_view.as_ref(),
2449 resolve_target: if is_last_axes {
2450 Some(target_view)
2451 } else {
2452 None
2453 },
2454 }
2455 } else {
2456 RenderTarget {
2457 view: target_view,
2458 resolve_target: None,
2459 }
2460 };
2461 let _ = self.render_camera_to_target_viewport(
2462 encoder,
2463 render_target,
2464 *viewport,
2465 &cfg,
2466 &cam,
2467 ax_idx,
2468 is_first_axes,
2469 )?;
2470 log::debug!(
2471 "runmat-plot: renderer.axes_to_viewports.axes_render_ok axes_index={}",
2472 ax_idx
2473 );
2474
2475 for id in hidden_ids {
2477 if let Some(node) = self.scene.get_node_mut(id) {
2478 node.visible = true;
2479 }
2480 }
2481 }
2482 log::debug!("runmat-plot: renderer.axes_to_viewports.ok");
2483 Ok(())
2484 }
2485
2486 fn create_default_camera() -> Camera {
2488 let mut camera = Camera::new();
2489 camera.projection = crate::core::camera::ProjectionType::Orthographic {
2490 left: -5.0,
2491 right: 5.0,
2492 bottom: -5.0,
2493 top: 5.0,
2494 near: -10.0,
2496 far: 10.0,
2497 };
2498 camera.depth_mode = DepthMode::default();
2499 camera.position = Vec3::new(0.0, 0.0, 1.0);
2501 camera.target = Vec3::new(0.0, 0.0, 0.0);
2502 camera.up = Vec3::new(0.0, 1.0, 0.0);
2503 camera
2504 }
2505
2506 pub fn camera(&self) -> &Camera {
2510 self.axes_cameras
2511 .first()
2512 .expect("axes_cameras must contain at least one camera")
2513 }
2514
2515 pub fn camera_mut(&mut self) -> &mut Camera {
2517 self.axes_cameras
2518 .first_mut()
2519 .expect("axes_cameras must contain at least one camera")
2520 }
2521
2522 pub fn axes_camera(&self, axes_index: usize) -> Option<&Camera> {
2523 self.axes_cameras.get(axes_index)
2524 }
2525
2526 pub fn scene(&self) -> &Scene {
2528 &self.scene
2529 }
2530
2531 pub fn scene_statistics(&self) -> crate::core::SceneStatistics {
2533 self.scene.statistics()
2534 }
2535
2536 pub fn view_bounds(&self) -> Option<(f64, f64, f64, f64)> {
2538 match self.camera().projection {
2539 crate::core::camera::ProjectionType::Orthographic {
2540 left,
2541 right,
2542 bottom,
2543 top,
2544 ..
2545 } => Some((left as f64, right as f64, bottom as f64, top as f64)),
2546 _ => None,
2547 }
2548 }
2549
2550 pub fn overlay_show_grid(&self) -> bool {
2552 self.figure_show_grid
2553 }
2554 pub fn overlay_show_grid_for_axes(&self, axes_index: usize) -> bool {
2555 self.last_figure
2556 .as_ref()
2557 .and_then(|f| f.axes_metadata(axes_index))
2558 .map(|m| m.grid_enabled)
2559 .unwrap_or(self.figure_show_grid)
2560 }
2561 pub fn overlay_show_box(&self) -> bool {
2562 self.figure_show_box
2563 }
2564 pub fn overlay_show_box_for_axes(&self, axes_index: usize) -> bool {
2565 self.last_figure
2566 .as_ref()
2567 .and_then(|f| f.axes_metadata(axes_index))
2568 .map(|m| m.box_enabled)
2569 .unwrap_or(self.figure_show_box)
2570 }
2571 pub fn overlay_title(&self) -> Option<&String> {
2572 self.figure_title.as_ref()
2573 }
2574 pub fn overlay_title_for_axes(&self, axes_index: usize) -> Option<&String> {
2575 self.last_figure
2576 .as_ref()
2577 .and_then(|f| f.axes_metadata(axes_index))
2578 .and_then(|m| m.title.as_ref())
2579 }
2580 pub fn overlay_x_label(&self) -> Option<&String> {
2581 self.figure_x_label.as_ref()
2582 }
2583 pub fn overlay_x_label_for_axes(&self, axes_index: usize) -> Option<&String> {
2584 self.last_figure
2585 .as_ref()
2586 .and_then(|f| f.axes_metadata(axes_index))
2587 .and_then(|m| m.x_label.as_ref())
2588 }
2589 pub fn overlay_x_label_style_for_axes(&self, axes_index: usize) -> Option<&TextStyle> {
2590 self.last_figure
2591 .as_ref()
2592 .and_then(|f| f.axes_metadata(axes_index))
2593 .map(|m| &m.x_label_style)
2594 }
2595 pub fn overlay_y_label(&self) -> Option<&String> {
2596 self.figure_y_label.as_ref()
2597 }
2598 pub fn overlay_y_label_for_axes(&self, axes_index: usize) -> Option<&String> {
2599 self.last_figure
2600 .as_ref()
2601 .and_then(|f| f.axes_metadata(axes_index))
2602 .and_then(|m| m.y_label.as_ref())
2603 }
2604 pub fn overlay_y_label_style_for_axes(&self, axes_index: usize) -> Option<&TextStyle> {
2605 self.last_figure
2606 .as_ref()
2607 .and_then(|f| f.axes_metadata(axes_index))
2608 .map(|m| &m.y_label_style)
2609 }
2610 pub fn overlay_z_label(&self) -> Option<&String> {
2611 self.figure_z_label.as_ref()
2612 }
2613 pub fn overlay_z_label_for_axes(&self, axes_index: usize) -> Option<&String> {
2614 self.last_figure
2615 .as_ref()
2616 .and_then(|f| f.axes_metadata(axes_index))
2617 .and_then(|m| m.z_label.as_ref())
2618 }
2619 pub fn overlay_z_label_style_for_axes(&self, axes_index: usize) -> Option<&TextStyle> {
2620 self.last_figure
2621 .as_ref()
2622 .and_then(|f| f.axes_metadata(axes_index))
2623 .map(|m| &m.z_label_style)
2624 }
2625 pub fn active_axes_pie_labels(&self) -> Vec<(String, glam::Vec2)> {
2626 let Some(fig) = self.last_figure.as_ref() else {
2627 return Vec::new();
2628 };
2629 fig.pie_labels_for_axes(fig.active_axes_index)
2630 .into_iter()
2631 .map(|entry| (entry.label, entry.position))
2632 .collect()
2633 }
2634 pub fn pie_labels_for_axes(&self, axes_index: usize) -> Vec<(String, glam::Vec2)> {
2635 let Some(fig) = self.last_figure.as_ref() else {
2636 return Vec::new();
2637 };
2638 fig.pie_labels_for_axes(axes_index)
2639 .into_iter()
2640 .map(|entry| (entry.label, entry.position))
2641 .collect()
2642 }
2643
2644 pub fn world_text_annotations_for_axes(
2645 &self,
2646 axes_index: usize,
2647 ) -> Vec<(glam::Vec3, String, TextStyle)> {
2648 self.last_figure
2649 .as_ref()
2650 .map(|f| {
2651 f.axes_text_annotations(axes_index)
2652 .iter()
2653 .map(|annotation| {
2654 (
2655 annotation.position,
2656 annotation.text.clone(),
2657 annotation.style.clone(),
2658 )
2659 })
2660 .collect()
2661 })
2662 .unwrap_or_default()
2663 }
2664
2665 pub fn world_axis_label_annotations_for_axes(
2666 &self,
2667 axes_index: usize,
2668 ) -> Vec<(glam::Vec3, String, TextStyle)> {
2669 let Some(fig) = self.last_figure.as_ref() else {
2670 return Vec::new();
2671 };
2672 let Some(meta) = fig.axes_metadata(axes_index) else {
2673 return Vec::new();
2674 };
2675 let Some(bounds) = self.axes_bounds(axes_index) else {
2676 return Vec::new();
2677 };
2678 let dx = (bounds.max.x - bounds.min.x).abs().max(1.0e-3);
2679 let dy = (bounds.max.y - bounds.min.y).abs().max(1.0e-3);
2680 let dz = (bounds.max.z - bounds.min.z).abs().max(1.0e-3);
2681 let camera = self
2682 .axes_camera(axes_index)
2683 .or_else(|| Some(self.camera()))
2684 .expect("plot renderer must always have a camera");
2685 let center = (bounds.min + bounds.max) * 0.5;
2686 let cam_delta = camera.position - center;
2687 let sx = if cam_delta.x >= 0.0 { 1.0 } else { -1.0 };
2688 let sy = if cam_delta.y >= 0.0 { 1.0 } else { -1.0 };
2689 let sz = if cam_delta.z >= 0.0 { 1.0 } else { -1.0 };
2690 let x_anchor = glam::Vec3::new(bounds.min.x + dx * 0.82, bounds.min.y, bounds.min.z)
2691 + glam::Vec3::new(0.0, -sy * dy * 0.10, -sz * dz * 0.08);
2692 let y_anchor = glam::Vec3::new(bounds.min.x, bounds.min.y + dy * 0.82, bounds.min.z)
2693 + glam::Vec3::new(-sx * dx * 0.10, 0.0, -sz * dz * 0.08);
2694 let z_anchor = glam::Vec3::new(bounds.min.x, bounds.min.y, bounds.min.z + dz * 0.82)
2695 + glam::Vec3::new(-sx * dx * 0.08, -sy * dy * 0.08, 0.0);
2696 let mut out = Vec::new();
2697 if let Some(label) = meta.x_label.clone().filter(|s| !s.is_empty()) {
2698 out.push((x_anchor, label, meta.x_label_style.clone()));
2699 }
2700 if let Some(label) = meta.y_label.clone().filter(|s| !s.is_empty()) {
2701 out.push((y_anchor, label, meta.y_label_style.clone()));
2702 }
2703 if let Some(label) = meta.z_label.clone().filter(|s| !s.is_empty()) {
2704 out.push((z_anchor, label, meta.z_label_style.clone()));
2705 }
2706 out
2707 }
2708 pub fn overlay_show_legend(&self) -> bool {
2709 self.figure_show_legend
2710 }
2711 pub fn overlay_show_legend_for_axes(&self, axes_index: usize) -> bool {
2712 self.last_figure
2713 .as_ref()
2714 .and_then(|f| f.axes_metadata(axes_index))
2715 .map(|m| m.legend_enabled)
2716 .unwrap_or(self.figure_show_legend)
2717 }
2718 pub fn overlay_legend_entries(&self) -> &Vec<LegendEntry> {
2719 &self.legend_entries
2720 }
2721 pub fn overlay_legend_entries_for_axes(&self, axes_index: usize) -> Vec<LegendEntry> {
2722 self.last_figure
2723 .as_ref()
2724 .map(|f| f.legend_entries_for_axes(axes_index))
2725 .unwrap_or_default()
2726 }
2727 pub fn overlay_x_log(&self) -> bool {
2728 self.figure_x_log
2729 }
2730 pub fn overlay_x_log_for_axes(&self, axes_index: usize) -> bool {
2731 self.last_figure
2732 .as_ref()
2733 .and_then(|f| f.axes_metadata(axes_index))
2734 .map(|m| m.x_log)
2735 .unwrap_or(self.figure_x_log)
2736 }
2737 pub fn overlay_y_log(&self) -> bool {
2738 self.figure_y_log
2739 }
2740 pub fn overlay_y_log_for_axes(&self, axes_index: usize) -> bool {
2741 self.last_figure
2742 .as_ref()
2743 .and_then(|f| f.axes_metadata(axes_index))
2744 .map(|m| m.y_log)
2745 .unwrap_or(self.figure_y_log)
2746 }
2747 pub fn overlay_colormap(&self) -> ColorMap {
2748 self.figure_colormap
2749 }
2750 pub fn overlay_colorbar_enabled(&self) -> bool {
2751 self.figure_colorbar_enabled
2752 }
2753 pub fn figure_axes_grid(&self) -> (usize, usize) {
2755 self.last_figure
2756 .as_ref()
2757 .map(|f| f.axes_grid())
2758 .unwrap_or((1, 1))
2759 }
2760 pub fn overlay_categorical_labels(&self) -> Option<(bool, &Vec<String>)> {
2762 if let (Some(is_x), Some(labels)) = (
2763 &self.figure_categorical_is_x,
2764 &self.figure_categorical_labels,
2765 ) {
2766 Some((*is_x, labels))
2767 } else {
2768 None
2769 }
2770 }
2771
2772 pub fn overlay_categorical_labels_for_axes(
2773 &self,
2774 axes_index: usize,
2775 ) -> Option<(bool, Vec<String>)> {
2776 self.last_figure
2777 .as_ref()
2778 .and_then(|f| f.categorical_axis_labels_for_axes(axes_index))
2779 }
2780
2781 pub fn overlay_histogram_edges_for_axes(&self, axes_index: usize) -> Option<(bool, Vec<f64>)> {
2782 self.last_figure
2783 .as_ref()
2784 .and_then(|f| f.histogram_axis_edges_for_axes(axes_index))
2785 }
2786
2787 pub fn overlay_display_bounds_for_axes(
2789 &self,
2790 axes_index: usize,
2791 ) -> Option<(f64, f64, f64, f64)> {
2792 self.display_bounds_for_axes(axes_index)
2793 }
2794
2795 pub fn data_bounds(&self) -> Option<(f64, f64, f64, f64)> {
2797 let base = self.data_bounds;
2798 base.map(|(bx_min, bx_max, by_min, by_max)| {
2799 let (mut x_min, mut x_max) = (bx_min, bx_max);
2800 let (mut y_min, mut y_max) = (by_min, by_max);
2801 if let Some((xl, xr)) = self.figure_x_limits {
2802 x_min = xl;
2803 x_max = xr;
2804 }
2805 if let Some((yl, yr)) = self.figure_y_limits {
2806 y_min = yl;
2807 y_max = yr;
2808 }
2809 (x_min, x_max, y_min, y_max)
2810 })
2811 }
2812
2813 pub fn axes_camera_mut(&mut self, idx: usize) -> Option<&mut Camera> {
2815 self.axes_cameras.get_mut(idx)
2816 }
2817
2818 pub fn view_bounds_for_axes(&self, idx: usize) -> Option<(f64, f64, f64, f64)> {
2820 if let Some(cam) = self.axes_cameras.get(idx) {
2821 if let crate::core::camera::ProjectionType::Orthographic {
2822 left,
2823 right,
2824 bottom,
2825 top,
2826 ..
2827 } = cam.projection
2828 {
2829 return Some((left as f64, right as f64, bottom as f64, top as f64));
2830 }
2831 }
2832 None
2833 }
2834
2835 pub fn axes_bounds(&self, axes_index: usize) -> Option<crate::core::BoundingBox> {
2836 let mut min = Vec3::new(f32::INFINITY, f32::INFINITY, f32::INFINITY);
2837 let mut max = Vec3::new(f32::NEG_INFINITY, f32::NEG_INFINITY, f32::NEG_INFINITY);
2838 let mut saw_any = false;
2839
2840 for node in self.scene.get_visible_nodes() {
2841 if node.axes_index != axes_index {
2842 continue;
2843 }
2844 let Some(render_data) = &node.render_data else {
2845 continue;
2846 };
2847 if let Some(bounds) = render_data.bounds {
2848 min = min.min(bounds.min);
2849 max = max.max(bounds.max);
2850 saw_any = true;
2851 continue;
2852 }
2853 for v in &render_data.vertices {
2854 let p = Vec3::new(v.position[0], v.position[1], v.position[2]);
2855 min = min.min(p);
2856 max = max.max(p);
2857 saw_any = true;
2858 }
2859 }
2860
2861 if !saw_any {
2862 return None;
2863 }
2864 Some(crate::core::BoundingBox { min, max })
2865 }
2866
2867 pub fn export_figure_clone(&self) -> crate::plots::Figure {
2869 if let Some(f) = &self.last_figure {
2870 return f.clone();
2871 }
2872 let mut fig = crate::plots::Figure::new();
2874 fig.title = self.figure_title.clone();
2875 fig.x_label = self.figure_x_label.clone();
2876 fig.y_label = self.figure_y_label.clone();
2877 fig.legend_enabled = self.figure_show_legend;
2878 fig.grid_enabled = self.figure_show_grid;
2879 fig.box_enabled = self.figure_show_box;
2880 fig.x_limits = self.figure_x_limits;
2881 fig.y_limits = self.figure_y_limits;
2882 fig.x_log = self.figure_x_log;
2883 fig.y_log = self.figure_y_log;
2884 fig.axis_equal = self.figure_axis_equal;
2885 fig.colormap = self.figure_colormap;
2886 fig.colorbar_enabled = self.figure_colorbar_enabled;
2887 let (rows, cols) = self.figure_axes_grid();
2888 fig.set_subplot_grid(rows, cols);
2889 fig
2890 }
2891}
2892
2893pub mod plot_utils {
2895 pub fn generate_major_ticks(min: f64, max: f64) -> Vec<f64> {
2896 if !(min.is_finite() && max.is_finite()) || max <= min {
2897 return Vec::new();
2898 }
2899 let range = (max - min).max(1e-9);
2900 let step = calculate_tick_interval(range);
2901 if !(step.is_finite() && step > 0.0) {
2902 return Vec::new();
2903 }
2904
2905 let mut ticks = Vec::new();
2906 let mut value = (min / step).ceil() * step;
2907 let epsilon = range * 1e-6 + step * 1e-6;
2908 while value <= max + epsilon {
2909 let snapped = if value.abs() < epsilon { 0.0 } else { value };
2910 ticks.push(snapped);
2911 value += step;
2912 if ticks.len() > 64 {
2913 break;
2914 }
2915 }
2916
2917 let endpoint_tol = step * 0.18;
2918 let near_min = ticks.iter().any(|t| (*t - min).abs() <= endpoint_tol);
2919 let near_max = ticks.iter().any(|t| (*t - max).abs() <= endpoint_tol);
2920 if !near_min {
2921 ticks.insert(0, min);
2922 }
2923 if !near_max {
2924 ticks.push(max);
2925 }
2926
2927 ticks.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
2928 ticks.dedup_by(|a, b| (*a - *b).abs() <= endpoint_tol * 0.5);
2929 ticks
2930 }
2931
2932 pub fn calculate_tick_interval(range: f64) -> f64 {
2934 let magnitude = 10.0_f64.powf(range.log10().floor());
2935 let normalized = range / magnitude;
2936
2937 let nice_interval = if normalized <= 1.0 {
2938 0.2
2939 } else if normalized <= 2.0 {
2940 0.5
2941 } else if normalized <= 5.0 {
2942 1.0
2943 } else {
2944 2.0
2945 };
2946
2947 nice_interval * magnitude
2948 }
2949
2950 pub fn format_tick_label(value: f64) -> String {
2952 fn trim_fixed(mut s: String) -> String {
2953 if s.contains('.') {
2954 while s.ends_with('0') {
2955 s.pop();
2956 }
2957 if s.ends_with('.') {
2958 s.pop();
2959 }
2960 }
2961 if s == "-0" {
2962 "0".to_string()
2963 } else {
2964 s
2965 }
2966 }
2967
2968 if value.abs() < 0.001 {
2969 "0".to_string()
2970 } else if value.abs() >= 1000.0 || value.fract().abs() < 0.0005 {
2971 format!("{value:.0}")
2972 } else if value.abs() < 0.1 {
2973 trim_fixed(format!("{value:.3}"))
2974 } else if value.abs() < 10.0 {
2975 trim_fixed(format!("{value:.2}"))
2976 } else {
2977 trim_fixed(format!("{value:.1}"))
2978 }
2979 }
2980
2981 pub fn generate_grid_lines(
2983 bounds: (f64, f64, f64, f64),
2984 plot_rect: (f32, f32, f32, f32), ) -> Vec<(f32, f32, f32, f32)> {
2986 let (x_min, x_max, y_min, y_max) = bounds;
2988 let (left, right, bottom, top) = plot_rect;
2989
2990 let mut lines = Vec::new();
2991
2992 let x_range = x_max - x_min;
2994 let x_interval = calculate_tick_interval(x_range);
2995 let mut x_val = (x_min / x_interval).ceil() * x_interval;
2996
2997 while x_val <= x_max {
2998 let x_screen = left + ((x_val - x_min) / x_range) as f32 * (right - left);
2999 lines.push((x_screen, bottom, x_screen, top));
3000 x_val += x_interval;
3001 }
3002
3003 let y_range = y_max - y_min;
3005 let y_interval = calculate_tick_interval(y_range);
3006 let mut y_val = (y_min / y_interval).ceil() * y_interval;
3007
3008 while y_val <= y_max {
3009 let y_screen = bottom + ((y_val - y_min) / y_range) as f32 * (top - bottom);
3010 lines.push((left, y_screen, right, y_screen));
3011 y_val += y_interval;
3012 }
3013
3014 lines
3015 }
3016}