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