1use crate::core::renderer::Vertex;
8use crate::core::{BoundingBox, 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
18type ViewBounds2D = (f64, f64, f64, f64);
19type PerAxesViewBounds = Vec<Option<ViewBounds2D>>;
20
21#[derive(Clone, Debug)]
22struct CachedSceneBuffers {
23 vertex_signature: (usize, usize),
24 vertex_buffer: Arc<wgpu::Buffer>,
25 index_signature: Option<(usize, usize)>,
26 index_buffer: Option<Arc<wgpu::Buffer>>,
27}
28
29#[derive(Clone, Debug, PartialEq)]
30struct AxesViewContract {
31 rows: usize,
32 cols: usize,
33 axes: Vec<AxesViewContractEntry>,
34}
35
36#[derive(Clone, Debug, PartialEq)]
37struct AxesViewContractEntry {
38 has_3d_content: bool,
39 x_limits: Option<(f64, f64)>,
40 y_limits: Option<(f64, f64)>,
41 z_limits: Option<(f64, f64)>,
42 axis_equal: bool,
43 x_log: bool,
44 y_log: bool,
45 view_azimuth_deg: Option<f32>,
46 view_elevation_deg: Option<f32>,
47 view_revision: u64,
48}
49
50const PATCH_3D_ABS_EPSILON: f32 = 1e-9;
51const PATCH_3D_REL_EPSILON: f32 = 1e-6;
52const MAX_2D_GRID_LINES_PER_AXIS: usize = 4096;
53
54pub struct PlotRenderer {
56 pub wgpu_renderer: WgpuRenderer,
58
59 pub scene: Scene,
61
62 pub theme: crate::styling::PlotThemeConfig,
64
65 data_bounds: Option<(f64, f64, f64, f64)>,
67 needs_update: bool,
68
69 figure_title: Option<String>,
71 figure_sg_title: Option<String>,
72 figure_sg_title_style: TextStyle,
73 figure_x_label: Option<String>,
74 figure_y_label: Option<String>,
75 figure_z_label: Option<String>,
76 figure_show_grid: bool,
77 figure_show_minor_grid: bool,
78 figure_show_legend: bool,
79 figure_show_box: bool,
80 figure_x_limits: Option<(f64, f64)>,
81 figure_y_limits: Option<(f64, f64)>,
82 legend_entries: Vec<LegendEntry>,
83 figure_x_log: bool,
84 figure_y_log: bool,
85 figure_axis_equal: bool,
86 figure_colormap: ColorMap,
87 figure_colorbar_enabled: bool,
88 figure_categorical_is_x: Option<bool>,
90 figure_categorical_labels: Option<Vec<String>>,
91 axes_cameras: Vec<Camera>,
93 pub(crate) last_figure: Option<crate::plots::Figure>,
95
96 last_scene_viewport_px: Option<(u32, u32)>,
99 last_axes_plot_sizes_px: Option<Vec<(u32, u32)>>,
101 last_axes_view_bounds: Option<PerAxesViewBounds>,
103 last_axes_view_contract: Option<AxesViewContract>,
105
106 camera_auto_fit: bool,
108 axes_2d_camera_user_controlled: Vec<bool>,
111 axes_applied_view_revisions: Vec<Option<u64>>,
113 scene_buffer_cache: RefCell<HashMap<u64, CachedSceneBuffers>>,
115}
116
117#[derive(Debug, Clone)]
119pub struct PlotRenderConfig {
120 pub width: u32,
122 pub height: u32,
123
124 pub background_color: Vec4,
126
127 pub show_grid: bool,
129
130 pub show_axes: bool,
132
133 pub show_title: bool,
135
136 pub msaa_samples: u32,
138
139 pub depth_mode: DepthMode,
141
142 pub clip_policy: ClipPolicy,
144
145 pub theme: crate::styling::PlotThemeConfig,
147}
148
149impl Default for PlotRenderConfig {
150 fn default() -> Self {
151 Self {
152 width: 800,
153 height: 600,
154 background_color: Vec4::new(0.08, 0.09, 0.11, 1.0), show_grid: true,
156 show_axes: true,
157 show_title: true,
158 msaa_samples: 4,
159 depth_mode: DepthMode::default(),
160 clip_policy: ClipPolicy::default(),
161 theme: crate::styling::PlotThemeConfig::default(),
162 }
163 }
164}
165
166pub struct RenderTarget<'a> {
168 pub view: &'a wgpu::TextureView,
169 pub resolve_target: Option<&'a wgpu::TextureView>,
170}
171
172#[derive(Debug)]
174pub struct RenderResult {
175 pub success: bool,
177
178 pub data_bounds: Option<(f64, f64, f64, f64)>,
180
181 pub vertex_count: usize,
183 pub triangle_count: usize,
184 pub render_time_ms: f64,
185}
186
187impl PlotRenderer {
188 pub fn on_surface_config_updated(&mut self) {
193 let current = (
194 self.wgpu_renderer.surface_config.width.max(1),
195 self.wgpu_renderer.surface_config.height.max(1),
196 );
197 if self.last_scene_viewport_px == Some(current) {
198 return;
199 }
200 let Some(figure) = self.last_figure.clone() else {
201 self.last_scene_viewport_px = Some(current);
202 return;
203 };
204 self.set_figure(figure);
206 }
207
208 fn prepare_buffers_for_render_data(
209 &self,
210 node_id: u64,
211 render_data: &crate::core::RenderData,
212 ) -> Option<(Arc<wgpu::Buffer>, Option<Arc<wgpu::Buffer>>)> {
213 let mut cache = self.scene_buffer_cache.borrow_mut();
214 let vertex_signature = (
215 render_data.vertices.as_ptr() as usize,
216 render_data.vertices.len(),
217 );
218 let index_signature = render_data
219 .indices
220 .as_ref()
221 .map(|indices| (indices.as_ptr() as usize, indices.len()));
222
223 if let Some(cached) = cache.get(&node_id) {
224 if cached.vertex_signature == vertex_signature
225 && cached.index_signature == index_signature
226 {
227 return Some((cached.vertex_buffer.clone(), cached.index_buffer.clone()));
228 }
229 }
230
231 let vertex_buffer = self
232 .wgpu_renderer
233 .vertex_buffer_from_sources(render_data.gpu_vertices.as_ref(), &render_data.vertices)?;
234 let index_buffer = render_data
235 .indices
236 .as_ref()
237 .map(|indices| Arc::new(self.wgpu_renderer.create_index_buffer(indices)));
238
239 cache.insert(
240 node_id,
241 CachedSceneBuffers {
242 vertex_signature,
243 vertex_buffer: vertex_buffer.clone(),
244 index_signature,
245 index_buffer: index_buffer.clone(),
246 },
247 );
248
249 Some((vertex_buffer, index_buffer))
250 }
251
252 fn gpu_indirect_args(render_data: &crate::core::RenderData) -> Option<(&wgpu::Buffer, u64)> {
253 render_data
254 .gpu_vertices
255 .as_ref()
256 .and_then(|buf| buf.indirect.as_ref())
257 .map(|indirect| (indirect.args.as_ref(), indirect.offset))
258 }
259
260 pub async fn new(
262 device: Arc<wgpu::Device>,
263 queue: Arc<wgpu::Queue>,
264 surface_config: wgpu::SurfaceConfiguration,
265 ) -> Result<Self, Box<dyn std::error::Error>> {
266 let wgpu_renderer = WgpuRenderer::new(device, queue, surface_config).await;
267 let scene = Scene::new();
268 let theme = crate::styling::PlotThemeConfig::default();
269
270 Ok(Self {
271 wgpu_renderer,
272 scene,
273 theme,
274 data_bounds: None,
275 needs_update: true,
276 figure_title: None,
277 figure_sg_title: None,
278 figure_sg_title_style: TextStyle::default(),
279 figure_x_label: None,
280 figure_y_label: None,
281 figure_z_label: None,
282 figure_show_grid: true,
283 figure_show_minor_grid: false,
284 figure_show_legend: true,
285 figure_show_box: true,
286 figure_x_limits: None,
287 figure_y_limits: None,
288 legend_entries: Vec::new(),
289 figure_x_log: false,
290 figure_y_log: false,
291 figure_axis_equal: false,
292 figure_colormap: ColorMap::Parula,
293 figure_colorbar_enabled: false,
294 figure_categorical_is_x: None,
295 figure_categorical_labels: None,
296 axes_cameras: vec![Self::create_default_camera()],
297 last_figure: None,
298 last_scene_viewport_px: None,
299 last_axes_plot_sizes_px: None,
300 last_axes_view_bounds: None,
301 last_axes_view_contract: None,
302 camera_auto_fit: true,
303 axes_2d_camera_user_controlled: vec![false],
304 axes_applied_view_revisions: vec![None],
305 scene_buffer_cache: RefCell::new(HashMap::new()),
306 })
307 }
308
309 fn plot_element_is_3d(plot: &crate::plots::figure::PlotElement) -> bool {
310 match plot {
311 crate::plots::figure::PlotElement::Surface(surface) => !surface.image_mode,
312 crate::plots::figure::PlotElement::Patch(patch) => {
313 if patch.force_3d() {
314 return true;
315 }
316
317 let mut max_xy = 0.0_f32;
318 let mut max_z = 0.0_f32;
319 for point in patch.vertices() {
320 max_xy = max_xy.max(point.x.abs().max(point.y.abs()));
321 max_z = max_z.max(point.z.abs());
322 }
323
324 max_z > PATCH_3D_ABS_EPSILON.max(max_xy * PATCH_3D_REL_EPSILON)
325 }
326 crate::plots::figure::PlotElement::Line3(_) => true,
327 crate::plots::figure::PlotElement::Scatter3(_) => true,
328 crate::plots::figure::PlotElement::Contour(contour) => contour.is_3d(),
329 _ => false,
330 }
331 }
332
333 pub fn axes_has_3d_content(&self, axes_index: usize) -> bool {
334 self.last_figure
335 .as_ref()
336 .map(|figure| {
337 figure
338 .plots()
339 .zip(figure.plot_axes_indices().iter().copied())
340 .any(|(plot, plot_axes_index)| {
341 plot_axes_index == axes_index && Self::plot_element_is_3d(plot)
342 })
343 })
344 .unwrap_or(false)
345 }
346
347 fn axes_view_contract_for_figure(figure: &Figure) -> AxesViewContract {
348 let (rows, cols) = figure.axes_grid();
349 let axes_count = rows.max(1) * cols.max(1);
350 let mut has_3d_content = vec![false; axes_count];
351 for (plot, axes_index) in figure
352 .plots()
353 .zip(figure.plot_axes_indices().iter().copied())
354 {
355 if axes_index < axes_count && Self::plot_element_is_3d(plot) {
356 has_3d_content[axes_index] = true;
357 }
358 }
359 let axes = (0..axes_count)
360 .map(|axes_index| {
361 let meta = figure.axes_metadata(axes_index);
362 AxesViewContractEntry {
363 has_3d_content: has_3d_content[axes_index],
364 x_limits: meta.and_then(|m| m.x_limits),
365 y_limits: meta.and_then(|m| m.y_limits),
366 z_limits: meta.and_then(|m| m.z_limits),
367 axis_equal: meta.map(|m| m.axis_equal).unwrap_or(false),
368 x_log: meta.map(|m| m.x_log).unwrap_or(false),
369 y_log: meta.map(|m| m.y_log).unwrap_or(false),
370 view_azimuth_deg: meta.and_then(|m| m.view_azimuth_deg),
371 view_elevation_deg: meta.and_then(|m| m.view_elevation_deg),
372 view_revision: meta.map(|m| m.view_revision).unwrap_or(0),
373 }
374 })
375 .collect();
376 AxesViewContract { rows, cols, axes }
377 }
378
379 pub fn note_camera_interaction(&mut self) {
381 if self.camera_auto_fit {
382 log::debug!(target: "runmat_plot", "camera_auto_fit disabled (user interaction)");
383 }
384 self.camera_auto_fit = false;
385 }
386
387 pub fn note_axes_camera_interaction(&mut self, axes_index: usize) {
388 self.note_camera_interaction();
389 if self.axes_has_3d_content(axes_index) {
390 return;
391 }
392 if let Some(flag) = self.axes_2d_camera_user_controlled.get_mut(axes_index) {
393 *flag = true;
394 }
395 }
396
397 pub fn set_axes_camera_interaction_flags(&mut self, flags: &[bool]) {
398 self.axes_2d_camera_user_controlled
399 .resize(self.axes_cameras.len(), false);
400 let mut any_user_controlled = false;
401 for idx in 0..self.axes_cameras.len() {
402 let controlled = flags.get(idx).copied().unwrap_or(false);
403 if let Some(flag) = self.axes_2d_camera_user_controlled.get_mut(idx) {
404 *flag = controlled;
405 }
406 any_user_controlled |= controlled;
407 }
408 if any_user_controlled {
409 self.note_camera_interaction();
410 }
411 }
412
413 fn clear_axes_camera_interaction(&mut self, axes_index: usize) {
414 if let Some(flag) = self.axes_2d_camera_user_controlled.get_mut(axes_index) {
415 *flag = false;
416 }
417 }
418
419 fn clear_all_axes_camera_interaction(&mut self) {
420 for flag in &mut self.axes_2d_camera_user_controlled {
421 *flag = false;
422 }
423 }
424
425 pub fn set_figure(&mut self, figure: Figure) {
427 self.scene.clear();
429 self.scene_buffer_cache.borrow_mut().clear();
430
431 self.cache_figure_meta(&figure);
433 self.last_figure = Some(figure.clone());
434 self.last_axes_plot_sizes_px = None;
435 self.last_axes_view_bounds = None;
436 let (rows, cols) = figure.axes_grid();
438 let num_axes = rows.max(1) * cols.max(1);
439 let axes_view_contract = Self::axes_view_contract_for_figure(&figure);
440 let axes_view_contract_changed =
441 self.last_axes_view_contract.as_ref() != Some(&axes_view_contract);
442 if axes_view_contract_changed {
443 log::debug!(
444 target: "runmat_plot.camera_refit",
445 "figure axes view contract changed; resetting script-owned camera fit rows={} cols={} axes_count={}",
446 rows,
447 cols,
448 num_axes
449 );
450 self.clear_all_axes_camera_interaction();
451 self.camera_auto_fit = true;
452 }
453 self.last_axes_view_contract = Some(axes_view_contract);
454
455 if self.axes_cameras.len() != num_axes {
456 self.axes_cameras
457 .resize_with(num_axes, Self::create_default_camera);
458 self.axes_2d_camera_user_controlled.resize(num_axes, false);
459 self.axes_applied_view_revisions.resize(num_axes, None);
460 self.camera_auto_fit = true;
461 }
462
463 for axes_index in 0..num_axes {
464 let wants_3d = self.axes_has_3d_content(axes_index);
465 let has_3d_camera = self
466 .axes_cameras
467 .get(axes_index)
468 .map(|cam| {
469 matches!(
470 cam.projection,
471 crate::core::camera::ProjectionType::Perspective { .. }
472 )
473 })
474 .unwrap_or(false);
475 if wants_3d != has_3d_camera {
476 self.axes_cameras[axes_index] = if wants_3d {
477 Camera::new()
478 } else {
479 Self::create_default_camera()
480 };
481 self.clear_axes_camera_interaction(axes_index);
482 if let Some(revision) = self.axes_applied_view_revisions.get_mut(axes_index) {
483 *revision = None;
484 }
485 self.camera_auto_fit = true;
486 }
487 }
488
489 self.add_figure_to_scene(figure);
490
491 self.needs_update = true;
493
494 let fit_applied = if self.camera_auto_fit {
496 if num_axes > 1 {
497 self.fit_cameras_to_axes_data()
498 } else {
499 self.fit_camera_to_data()
500 }
501 } else {
502 false
503 };
504 if self.camera_auto_fit && fit_applied {
505 self.camera_auto_fit = false;
508 }
509 self.apply_stored_axes_views();
510 }
511
512 fn add_figure_to_scene(&mut self, figure: Figure) {
514 self.add_figure_to_scene_with_axes_plot_sizes(figure, None);
515 }
516
517 fn add_figure_to_scene_with_axes_plot_sizes(
518 &mut self,
519 mut figure: Figure,
520 axes_plot_sizes_px: Option<&[(u32, u32)]>,
521 ) {
522 use crate::core::SceneNode;
523
524 let (rows, cols) = figure.axes_grid();
525
526 let viewport_px = (
530 self.wgpu_renderer.surface_config.width.max(1),
531 self.wgpu_renderer.surface_config.height.max(1),
532 );
533 self.last_scene_viewport_px = Some(viewport_px);
534 let gpu = crate::core::GpuPackContext {
535 device: &self.wgpu_renderer.device,
536 queue: &self.wgpu_renderer.queue,
537 };
538 let view_bounds = self.axes_view_bounds_for_count(rows.max(1) * cols.max(1));
539 let viewport_hint = if axes_plot_sizes_px.is_some() || rows.max(1) * cols.max(1) <= 1 {
540 Some(viewport_px)
541 } else {
542 None
543 };
544 let render_data_list = figure.render_data_with_axes_with_viewport_and_gpu(
545 viewport_hint,
546 axes_plot_sizes_px,
547 Some(&view_bounds),
548 Some(&gpu),
549 );
550
551 for (node_id_counter, (axes_index, render_data)) in render_data_list.into_iter().enumerate()
552 {
553 let axes_index = axes_index.min(rows * cols - 1);
554 let node = SceneNode {
556 id: node_id_counter as u64,
557 name: format!("Plot {node_id_counter} @axes {axes_index}"),
558 transform: Mat4::IDENTITY,
559 visible: true,
560 cast_shadows: false,
561 receive_shadows: false,
562 axes_index,
563 parent: None,
564 children: Vec::new(),
565 render_data: Some(render_data),
566 bounds: crate::core::BoundingBox::default(),
567 lod_levels: Vec::new(),
568 current_lod: 0,
569 };
570
571 let nid = self.scene.add_node(node);
572 let _ = nid;
574 let _ = axes_index;
575 let _ = rows;
576 let _ = cols;
577 }
578 }
579
580 pub fn ensure_scene_viewport_dependent_geometry_for_axes(
581 &mut self,
582 axes_plot_sizes_px: &[(u32, u32)],
583 ) {
584 let normalized: Vec<(u32, u32)> = axes_plot_sizes_px
585 .iter()
586 .map(|&(w, h)| (w.max(1), h.max(1)))
587 .collect();
588 if normalized.iter().any(|&(w, h)| w < 2 || h < 2) {
589 log::debug!(
590 target: "runmat_plot.viewport_rebuild",
591 "skipped viewport-dependent scene geometry rebuild for unstable viewport_sizes={:?}",
592 normalized
593 );
594 return;
595 }
596 let view_bounds = self.axes_view_bounds_for_count(normalized.len().max(1));
597 if self.last_axes_plot_sizes_px.as_ref() == Some(&normalized)
598 && self.last_axes_view_bounds.as_ref() == Some(&view_bounds)
599 {
600 return;
601 }
602 let Some(figure) = self.last_figure.clone() else {
603 self.last_axes_plot_sizes_px = Some(normalized);
604 self.last_axes_view_bounds = Some(view_bounds);
605 return;
606 };
607 self.scene.clear();
608 self.scene_buffer_cache.borrow_mut().clear();
609 self.add_figure_to_scene_with_axes_plot_sizes(figure, Some(&normalized));
610 log::debug!(
611 target: "runmat_plot.viewport_rebuild",
612 "rebuilt viewport-dependent scene geometry axes_count={} viewport_sizes={:?}",
613 normalized.len(),
614 normalized
615 );
616 self.refit_2d_cameras_to_scene_bounds();
617 self.last_axes_plot_sizes_px = Some(normalized);
618 self.last_axes_view_bounds = Some(view_bounds);
619 self.needs_update = true;
620 }
621
622 fn axes_view_bounds_for_count(&self, axes_count: usize) -> PerAxesViewBounds {
623 (0..axes_count)
624 .map(|idx| self.view_bounds_for_axes(idx))
625 .collect()
626 }
627
628 fn refit_2d_cameras_to_scene_bounds(&mut self) {
629 for idx in 0..self.axes_cameras.len() {
630 if self.axes_has_3d_content(idx) {
631 continue;
632 }
633 if self
634 .axes_2d_camera_user_controlled
635 .get(idx)
636 .copied()
637 .unwrap_or(false)
638 {
639 continue;
640 }
641 let Some((x_min, x_max, y_min, y_max)) = self.display_bounds_for_axes(idx) else {
642 continue;
643 };
644 let geometry_bounds = self.axes_bounds(idx);
645 let Some(cam) = self.axes_cameras.get_mut(idx) else {
646 continue;
647 };
648 if let crate::core::camera::ProjectionType::Orthographic {
649 ref mut left,
650 ref mut right,
651 ref mut bottom,
652 ref mut top,
653 ..
654 } = cam.projection
655 {
656 *left = x_min as f32;
657 *right = x_max as f32;
658 *bottom = y_min as f32;
659 *top = y_max as f32;
660 let camera_left = *left;
661 let camera_right = *right;
662 let camera_bottom = *bottom;
663 let camera_top = *top;
664 cam.position.z = 1.0;
665 cam.target.z = 0.0;
666 cam.mark_dirty();
667 if let Some(bounds) = geometry_bounds {
668 log::debug!(
669 target: "runmat_plot.camera_refit",
670 "refit 2d camera to rebuilt scene bounds axes_index={} geometry=({}, {})..({}, {}) camera=({}, {})..({}, {}) margins=top:{} bottom:{} left:{} right:{}",
671 idx,
672 bounds.min.x,
673 bounds.min.y,
674 bounds.max.x,
675 bounds.max.y,
676 camera_left,
677 camera_bottom,
678 camera_right,
679 camera_top,
680 camera_top - bounds.max.y,
681 bounds.min.y - camera_bottom,
682 bounds.min.x - camera_left,
683 camera_right - bounds.max.x
684 );
685 } else {
686 log::debug!(
687 target: "runmat_plot.camera_refit",
688 "refit 2d camera without geometry bounds axes_index={} camera=({}, {})..({}, {})",
689 idx,
690 camera_left,
691 camera_bottom,
692 camera_right,
693 camera_top
694 );
695 }
696 if let Some(display_bounds) = self.display_bounds_for_axes(idx) {
697 log::debug!(
698 target: "runmat_plot.bounds_chain",
699 "bounds chain axes_index={} axes_bounds=({}, {})..({}, {}) display_bounds=({}, {})..({}, {}) camera_bounds=({}, {})..({}, {})",
700 idx,
701 geometry_bounds.map(|b| b.min.x as f64).unwrap_or(f64::NAN),
702 geometry_bounds.map(|b| b.min.y as f64).unwrap_or(f64::NAN),
703 geometry_bounds.map(|b| b.max.x as f64).unwrap_or(f64::NAN),
704 geometry_bounds.map(|b| b.max.y as f64).unwrap_or(f64::NAN),
705 display_bounds.0,
706 display_bounds.2,
707 display_bounds.1,
708 display_bounds.3,
709 camera_left,
710 camera_bottom,
711 camera_right,
712 camera_top
713 );
714 }
715 }
716 }
717 }
718
719 fn cache_figure_meta(&mut self, figure: &Figure) {
721 self.figure_title = figure.title.clone();
722 self.figure_sg_title = figure.sg_title.clone();
723 self.figure_sg_title_style = figure.sg_title_style.clone();
724 self.figure_x_label = figure.x_label.clone();
725 self.figure_y_label = figure.y_label.clone();
726 self.figure_z_label = figure.z_label.clone();
727 self.figure_show_grid = figure.grid_enabled;
728 self.figure_show_minor_grid = figure.minor_grid_enabled;
729 self.figure_show_legend = figure.legend_enabled;
730 self.figure_show_box = figure.box_enabled;
731 self.figure_x_limits = figure.x_limits;
732 self.figure_y_limits = figure.y_limits;
733 self.legend_entries = figure.legend_entries();
734 self.figure_x_log = figure.x_log;
735 self.figure_y_log = figure.y_log;
736 self.figure_axis_equal = figure.axis_equal;
737 self.figure_colormap = figure.colormap;
738 self.figure_colorbar_enabled = figure.colorbar_enabled;
739 if let Some((is_x, labels)) = figure.categorical_axis_labels() {
741 self.figure_categorical_is_x = Some(is_x);
742 self.figure_categorical_labels = Some(labels);
743 } else {
744 self.figure_categorical_is_x = None;
745 self.figure_categorical_labels = None;
746 }
747 }
748
749 fn apply_stored_axes_views(&mut self) {
750 let Some(fig) = self.last_figure.as_ref() else {
751 return;
752 };
753 for (idx, cam) in self.axes_cameras.iter_mut().enumerate() {
754 if !matches!(
755 cam.projection,
756 crate::core::camera::ProjectionType::Perspective { .. }
757 ) {
758 continue;
759 }
760 if let Some(meta) = fig.axes_metadata(idx) {
761 if let (Some(az), Some(el)) = (meta.view_azimuth_deg, meta.view_elevation_deg) {
762 if self.axes_applied_view_revisions.get(idx).copied().flatten()
763 == Some(meta.view_revision)
764 {
765 continue;
766 }
767 cam.set_view_angles_deg(az, el);
768 if let Some(revision) = self.axes_applied_view_revisions.get_mut(idx) {
769 *revision = Some(meta.view_revision);
770 }
771 } else if let Some(revision) = self.axes_applied_view_revisions.get_mut(idx) {
772 *revision = None;
773 }
774 }
775 }
776 }
777
778 fn display_bounds_for_axes(&self, axes_index: usize) -> Option<(f64, f64, f64, f64)> {
779 let base = self.axes_bounds(axes_index)?;
780 let mut x_min = base.min.x as f64;
781 let mut x_max = base.max.x as f64;
782 let mut y_min = base.min.y as f64;
783 let mut y_max = base.max.y as f64;
784
785 if let Some(fig) = self.last_figure.as_ref() {
786 if let Some(meta) = fig.axes_metadata(axes_index) {
787 if let Some((xl, xr)) = meta.x_limits {
788 x_min = xl;
789 x_max = xr;
790 }
791 if let Some((yl, yr)) = meta.y_limits {
792 y_min = yl;
793 y_max = yr;
794 }
795 if meta.axis_equal {
796 let cx = (x_min + x_max) * 0.5;
797 let cy = (y_min + y_max) * 0.5;
798 let size = (x_max - x_min).abs().max((y_max - y_min).abs()).max(0.1);
799 x_min = cx - size * 0.5;
800 x_max = cx + size * 0.5;
801 y_min = cy - size * 0.5;
802 y_max = cy + size * 0.5;
803 }
804 }
805 }
806
807 Some((x_min, x_max, y_min, y_max))
808 }
809
810 fn apply_3d_display_limits_to_bounds(
811 bounds: BoundingBox,
812 figure: Option<&Figure>,
813 axes_index: usize,
814 ) -> BoundingBox {
815 let Some(meta) = figure.and_then(|fig| fig.axes_metadata(axes_index)) else {
816 return bounds;
817 };
818 let mut min = bounds.min;
819 let mut max = bounds.max;
820 if let Some((lo, hi)) = meta.x_limits {
821 min.x = lo as f32;
822 max.x = hi as f32;
823 }
824 if let Some((lo, hi)) = meta.y_limits {
825 min.y = lo as f32;
826 max.y = hi as f32;
827 }
828 if let Some((lo, hi)) = meta.z_limits {
829 min.z = lo as f32;
830 max.z = hi as f32;
831 }
832 BoundingBox { min, max }
833 }
834
835 fn bounds_are_finite(bounds: BoundingBox) -> bool {
836 bounds.min.x.is_finite()
837 && bounds.min.y.is_finite()
838 && bounds.min.z.is_finite()
839 && bounds.max.x.is_finite()
840 && bounds.max.y.is_finite()
841 && bounds.max.z.is_finite()
842 }
843
844 fn current_3d_display_bounds_for_axes(&self, axes_index: usize) -> Option<BoundingBox> {
845 let bounds = self.axes_bounds(axes_index)?;
846 let bounds =
847 Self::apply_3d_display_limits_to_bounds(bounds, self.last_figure.as_ref(), axes_index);
848 Self::bounds_are_finite(bounds).then_some(bounds)
849 }
850
851 fn display_bounds_3d_for_axes(&self, axes_index: usize) -> Option<BoundingBox> {
852 self.current_3d_display_bounds_for_axes(axes_index)
853 }
854
855 fn axes_model_matrix(&self, _axes_index: usize) -> Mat4 {
856 Mat4::IDENTITY
857 }
858
859 fn fit_cameras_to_axes_data(&mut self) -> bool {
860 let mut applied = false;
861 for idx in 0..self.axes_cameras.len() {
862 if self.axes_has_3d_content(idx) {
863 let Some(bounds) = self.display_bounds_3d_for_axes(idx) else {
864 continue;
865 };
866 let center = (bounds.min + bounds.max) * 0.5;
867 let mut cam = Camera::new();
868 cam.target = center;
869 cam.up = Vec3::Z;
870 cam.position = center + Vec3::new(1.0, -1.0, 1.0);
871 cam.fit_bounds(bounds.min, bounds.max);
872 self.axes_cameras[idx] = cam;
873 applied = true;
874 continue;
875 }
876
877 let Some((x_min, x_max, y_min, y_max)) = self.display_bounds_for_axes(idx) else {
878 continue;
879 };
880 let mut cam = Self::create_default_camera();
881 if let crate::core::camera::ProjectionType::Orthographic {
882 ref mut left,
883 ref mut right,
884 ref mut bottom,
885 ref mut top,
886 ..
887 } = cam.projection
888 {
889 *left = x_min as f32;
890 *right = x_max as f32;
891 *bottom = y_min as f32;
892 *top = y_max as f32;
893 }
894 cam.position.z = 1.0;
895 cam.target.z = 0.0;
896 cam.mark_dirty();
897 self.axes_cameras[idx] = cam;
898 applied = true;
899 }
900 applied
901 }
902
903 pub fn calculate_data_bounds(&mut self) -> Option<(f64, f64, f64, f64)> {
905 let mut min_x = f64::INFINITY;
906 let mut max_x = f64::NEG_INFINITY;
907 let mut min_y = f64::INFINITY;
908 let mut max_y = f64::NEG_INFINITY;
909
910 for node in self.scene.get_visible_nodes() {
911 if let Some(render_data) = &node.render_data {
912 if let Some(bounds) = render_data.bounds {
913 min_x = min_x.min(bounds.min.x as f64);
914 max_x = max_x.max(bounds.max.x as f64);
915 min_y = min_y.min(bounds.min.y as f64);
916 max_y = max_y.max(bounds.max.y as f64);
917 continue;
918 }
919 for vertex in &render_data.vertices {
920 let x = vertex.position[0] as f64;
921 let y = vertex.position[1] as f64;
922 min_x = min_x.min(x);
923 max_x = max_x.max(x);
924 min_y = min_y.min(y);
925 max_y = max_y.max(y);
926 }
927 }
928 }
929
930 if min_x != f64::INFINITY && max_x != f64::NEG_INFINITY {
931 let x_range = (max_x - min_x).max(0.1);
934 let y_range = (max_y - min_y).max(0.1);
935 let x_margin = x_range * 0.04;
936 let y_margin = y_range * 0.04;
937
938 let bounds = (
939 min_x - x_margin,
940 max_x + x_margin,
941 min_y - y_margin,
942 max_y + y_margin,
943 );
944
945 self.data_bounds = Some(bounds);
947 Some(bounds)
948 } else {
949 self.data_bounds = None;
950 None
951 }
952 }
953
954 pub fn fit_camera_to_data(&mut self) -> bool {
958 if self.axes_cameras.len() > 1 {
959 return self.fit_cameras_to_axes_data();
960 }
961
962 if self.axes_has_3d_content(0) {
963 let Some(bounds) = self.display_bounds_3d_for_axes(0) else {
964 return false;
965 };
966 let center = (bounds.min + bounds.max) * 0.5;
967 let mut cam = Camera::new();
968 cam.target = center;
969 cam.up = Vec3::Z;
970 cam.position = center + Vec3::new(1.0, -1.0, 1.0);
971 cam.fit_bounds(bounds.min, bounds.max);
972 if let Some(axis_cam) = self.axes_cameras.get_mut(0) {
973 *axis_cam = cam;
974 }
975 return true;
976 }
977
978 if let Some((x_min, x_max, y_min, y_max)) = self.display_bounds_for_axes(0) {
979 let mut cam = Self::create_default_camera();
981 let l = x_min as f32;
982 let r = x_max as f32;
983 let b = y_min as f32;
984 let t = y_max as f32;
985 if let crate::core::camera::ProjectionType::Orthographic {
986 ref mut left,
987 ref mut right,
988 ref mut bottom,
989 ref mut top,
990 ..
991 } = cam.projection
992 {
993 *left = l;
994 *right = r;
995 *bottom = b;
996 *top = t;
997 }
998 cam.position.z = 1.0;
999 cam.target.z = 0.0;
1000 cam.mark_dirty();
1001
1002 if let Some(axis_cam) = self.axes_cameras.get_mut(0) {
1003 *axis_cam = cam;
1004 }
1005 return true;
1006 }
1007 false
1008 }
1009
1010 pub fn fit_extents(&mut self) {
1012 let _ = if self.figure_axes_grid().0 * self.figure_axes_grid().1 > 1 {
1013 self.fit_cameras_to_axes_data()
1014 } else {
1015 self.fit_camera_to_data()
1016 };
1017 self.clear_all_axes_camera_interaction();
1018 self.camera_auto_fit = false;
1019 self.needs_update = true;
1020 }
1021
1022 pub fn reset_camera_position(&mut self) {
1028 let dir = Vec3::new(1.0, -1.0, 1.0).normalize_or_zero();
1029 let data_centers: Vec<Vec3> = (0..self.axes_cameras.len())
1030 .map(|idx| {
1031 if self.axes_has_3d_content(idx) {
1032 self.display_bounds_3d_for_axes(idx)
1033 } else {
1034 self.axes_bounds(idx)
1035 }
1036 .map(|b| (b.min + b.max) * 0.5)
1037 .unwrap_or_else(|| self.axes_cameras[idx].target)
1038 })
1039 .collect();
1040 let display_bounds: PerAxesViewBounds = (0..self.axes_cameras.len())
1041 .map(|idx| self.display_bounds_for_axes(idx))
1042 .collect();
1043 for (idx, c) in self.axes_cameras.iter_mut().enumerate() {
1044 if matches!(
1045 c.projection,
1046 crate::core::camera::ProjectionType::Perspective { .. }
1047 ) {
1048 let data_center = data_centers.get(idx).copied().unwrap_or(c.target);
1049 let dist = (c.position - c.target).length().max(0.1);
1050 c.target = data_center;
1051 c.up = Vec3::Z;
1052 c.position = data_center + dir * dist;
1053 c.mark_dirty();
1054 } else if let Some((x_min, x_max, y_min, y_max)) = display_bounds[idx] {
1055 let mut cam = Self::create_default_camera();
1056 if let crate::core::camera::ProjectionType::Orthographic {
1057 ref mut left,
1058 ref mut right,
1059 ref mut bottom,
1060 ref mut top,
1061 ..
1062 } = cam.projection
1063 {
1064 *left = x_min as f32;
1065 *right = x_max as f32;
1066 *bottom = y_min as f32;
1067 *top = y_max as f32;
1068 }
1069 cam.position.z = 1.0;
1070 cam.target.z = 0.0;
1071 cam.mark_dirty();
1072 *c = cam;
1073 }
1074 }
1075 self.clear_all_axes_camera_interaction();
1076 self.camera_auto_fit = false;
1077 self.needs_update = true;
1078 }
1079
1080 pub fn render_to_viewport(
1082 &mut self,
1083 encoder: &mut wgpu::CommandEncoder,
1084 target_view: &wgpu::TextureView,
1085 _viewport: (f32, f32, f32, f32), clear_background: bool,
1087 background_color: Option<glam::Vec4>,
1088 ) -> Result<RenderResult, Box<dyn std::error::Error>> {
1089 let start_time = Instant::now();
1090
1091 let mut render_items = Vec::new();
1093 let mut total_vertices = 0;
1094 let mut total_triangles = 0;
1095
1096 for node in self.scene.get_visible_nodes() {
1097 if let Some(render_data) = &node.render_data {
1098 if let Some(vertex_buffer) = self.wgpu_renderer.vertex_buffer_from_sources(
1099 render_data.gpu_vertices.as_ref(),
1100 &render_data.vertices,
1101 ) {
1102 self.wgpu_renderer
1103 .ensure_pipeline(render_data.pipeline_type);
1104
1105 log::trace!(
1106 target: "runmat_plot",
1107 "upload vertices={}, draw_calls={}",
1108 render_data.vertex_count(),
1109 render_data.draw_calls.len()
1110 );
1111
1112 render_items.push((render_data, vertex_buffer));
1113 total_vertices += render_data.vertex_count();
1114
1115 if render_data.pipeline_type == crate::core::PipelineType::Triangles {
1116 total_triangles += render_data.vertex_count() / 3;
1117 }
1118 }
1119 }
1120 }
1121
1122 let mut cam = self.camera().clone();
1124 let view_proj_matrix = cam.view_proj_matrix();
1125
1126 self.wgpu_renderer
1127 .update_uniforms(view_proj_matrix, Mat4::IDENTITY);
1128
1129 let use_msaa = self.wgpu_renderer.msaa_sample_count > 1;
1131 let msaa_view_opt = if use_msaa {
1132 let tex = self
1133 .wgpu_renderer
1134 .device
1135 .create_texture(&wgpu::TextureDescriptor {
1136 label: Some("runmat_msaa_color_camera"),
1137 size: wgpu::Extent3d {
1138 width: self.wgpu_renderer.surface_config.width,
1139 height: self.wgpu_renderer.surface_config.height,
1140 depth_or_array_layers: 1,
1141 },
1142 mip_level_count: 1,
1143 sample_count: self.wgpu_renderer.msaa_sample_count,
1144 dimension: wgpu::TextureDimension::D2,
1145 format: self.wgpu_renderer.surface_config.format,
1146 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
1147 view_formats: &[],
1148 });
1149 Some(tex.create_view(&wgpu::TextureViewDescriptor::default()))
1150 } else {
1151 None
1152 };
1153
1154 let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1155 label: Some("Viewport Plot Render Pass"),
1156 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1157 view: msaa_view_opt.as_ref().unwrap_or(target_view),
1158 resolve_target: if use_msaa { Some(target_view) } else { None },
1159 ops: wgpu::Operations {
1160 load: if clear_background {
1161 wgpu::LoadOp::Clear(wgpu::Color {
1162 r: background_color.map_or(0.08, |c| c.x as f64),
1163 g: background_color.map_or(0.09, |c| c.y as f64),
1164 b: background_color.map_or(0.11, |c| c.z as f64),
1165 a: background_color.map_or(1.0, |c| c.w as f64),
1166 })
1167 } else {
1168 wgpu::LoadOp::Load
1169 },
1170 store: wgpu::StoreOp::Store,
1171 },
1172 })],
1173 depth_stencil_attachment: None,
1174 occlusion_query_set: None,
1175 timestamp_writes: None,
1176 });
1177
1178 let (vx, vy, vw, vh) = _viewport;
1180 render_pass.set_viewport(vx, vy, vw, vh, 0.0, 1.0);
1181
1182 let sw = self.wgpu_renderer.surface_config.width as f32;
1184 let sh = self.wgpu_renderer.surface_config.height as f32;
1185 let ndc_left = (vx / sw) * 2.0 - 1.0;
1186 let ndc_right = ((vx + vw) / sw) * 2.0 - 1.0;
1187 let ndc_top = 1.0 - (vy / sh) * 2.0;
1188 let ndc_bottom = 1.0 - ((vy + vh) / sh) * 2.0;
1189
1190 let (x_min, y_min, x_max, y_max) = (0.0_f64, 0.0_f64, 1.0_f64, 1.0_f64);
1192 self.wgpu_renderer.update_direct_uniforms(
1193 [x_min as f32, y_min as f32],
1194 [x_max as f32, y_max as f32],
1195 [ndc_left, ndc_bottom],
1196 [ndc_right, ndc_top],
1197 [sw, sh],
1198 );
1199
1200 drop(render_pass);
1202
1203 let render_time = start_time.elapsed().as_secs_f64() * 1000.0;
1204
1205 Ok(RenderResult {
1206 success: true,
1207 data_bounds: self.data_bounds,
1208 vertex_count: total_vertices,
1209 triangle_count: total_triangles,
1210 render_time_ms: render_time,
1211 })
1212 }
1213
1214 pub fn render(
1216 &mut self,
1217 encoder: &mut wgpu::CommandEncoder,
1218 target: RenderTarget<'_>,
1219 config: &PlotRenderConfig,
1220 ) -> Result<RenderResult, Box<dyn std::error::Error>> {
1221 let start_time = Instant::now();
1222
1223 self.wgpu_renderer.ensure_msaa(config.msaa_samples);
1224
1225 let aspect_ratio = config.width as f32 / config.height as f32;
1227 let mut cam = self.camera().clone();
1228 cam.update_aspect_ratio(aspect_ratio);
1229 let view_proj_matrix = cam.view_proj_matrix();
1230 let model_matrix = self.axes_model_matrix(0);
1231 self.wgpu_renderer
1232 .update_uniforms(view_proj_matrix, model_matrix);
1233
1234 let mut render_items = Vec::new();
1236 let mut total_vertices = 0;
1237 let mut total_triangles = 0;
1238
1239 for node in self.scene.get_visible_nodes() {
1240 if let Some(render_data) = &node.render_data {
1241 if let Some((vertex_buffer, index_buffer)) =
1242 self.prepare_buffers_for_render_data(node.id, render_data)
1243 {
1244 self.wgpu_renderer
1245 .ensure_pipeline(render_data.pipeline_type);
1246 render_items.push((render_data, vertex_buffer, index_buffer));
1247
1248 total_vertices += render_data.vertex_count();
1249 if let Some(indices) = &render_data.indices {
1250 total_triangles += indices.len() / 3;
1251 }
1252 }
1253 }
1254 }
1255
1256 let mut image_bind_groups: Vec<Option<wgpu::BindGroup>> =
1258 Vec::with_capacity(render_items.len());
1259 let has_textured_items = render_items.iter().any(|(render_data, _, _)| {
1260 render_data.pipeline_type == crate::core::PipelineType::Textured
1261 });
1262 if has_textured_items {
1263 self.wgpu_renderer.ensure_image_pipeline();
1265 let mut inferred_bounds: Option<(f64, f64, f64, f64)> = None;
1266 for (render_data, _, _) in &render_items {
1267 let Some(bounds) = render_data.bounds.as_ref() else {
1268 continue;
1269 };
1270 let min_x = bounds.min.x as f64;
1271 let max_x = bounds.max.x as f64;
1272 let min_y = bounds.min.y as f64;
1273 let max_y = bounds.max.y as f64;
1274 inferred_bounds = Some(match inferred_bounds {
1275 Some((x0, x1, y0, y1)) => {
1276 (x0.min(min_x), x1.max(max_x), y0.min(min_y), y1.max(max_y))
1277 }
1278 None => (min_x, max_x, min_y, max_y),
1279 });
1280 }
1281
1282 let (mut x_min, mut x_max, mut y_min, mut y_max) = self
1283 .data_bounds
1284 .or(inferred_bounds)
1285 .unwrap_or((-1.0, 1.0, -1.0, 1.0));
1286 if (x_max - x_min).abs() < f64::EPSILON {
1288 x_min -= 0.5;
1289 x_max += 0.5;
1290 }
1291 if (y_max - y_min).abs() < f64::EPSILON {
1292 y_min -= 0.5;
1293 y_max += 0.5;
1294 }
1295 log::trace!(
1296 target: "runmat_plot",
1297 "direct uniforms bounds x=({}, {}) y=({}, {}) size=({}, {})",
1298 x_min,
1299 x_max,
1300 y_min,
1301 y_max,
1302 config.width,
1303 config.height
1304 );
1305 self.wgpu_renderer.update_direct_uniforms(
1306 [x_min as f32, y_min as f32],
1307 [x_max as f32, y_max as f32],
1308 [-1.0, -1.0],
1309 [1.0, 1.0],
1310 [config.width as f32, config.height as f32],
1311 );
1312 }
1313 for (render_data, _vb, _ib) in &render_items {
1314 if render_data.pipeline_type == crate::core::PipelineType::Textured {
1315 if let Some(crate::core::scene::ImageData::Rgba8 {
1316 width,
1317 height,
1318 data,
1319 }) = &render_data.image
1320 {
1321 let (_tex, _view, img_bg) = self
1322 .wgpu_renderer
1323 .create_image_texture_and_bind_group(*width, *height, data);
1324 image_bind_groups.push(Some(img_bg));
1325 } else {
1326 image_bind_groups.push(None);
1327 }
1328 } else {
1329 image_bind_groups.push(None);
1330 }
1331 }
1332 let mut point_style_bind_groups: Vec<Option<wgpu::BindGroup>> =
1333 Vec::with_capacity(render_items.len());
1334 for (render_data, _vb, _ib) in &render_items {
1335 if matches!(
1336 render_data.pipeline_type,
1337 crate::core::PipelineType::Points | crate::core::PipelineType::Scatter3
1338 ) {
1339 let style = crate::core::renderer::PointStyleUniforms {
1340 face_color: render_data.material.albedo.to_array(),
1341 edge_color: render_data.material.emissive.to_array(),
1342 edge_thickness_px: render_data.material.roughness,
1343 marker_shape: render_data.material.metallic as u32,
1344 _pad: [0.0, 0.0],
1345 };
1346 let (_buf, bg) = self.wgpu_renderer.create_point_style_bind_group(style);
1347 point_style_bind_groups.push(Some(bg));
1348 } else {
1349 point_style_bind_groups.push(None);
1350 }
1351 }
1352 let mut point_buffers: Vec<Option<(wgpu::Buffer, usize)>> =
1354 Vec::with_capacity(render_items.len());
1355 for (render_data, _vb, _ib) in &render_items {
1356 if matches!(
1357 render_data.pipeline_type,
1358 crate::core::PipelineType::Points | crate::core::PipelineType::Scatter3
1359 ) && !render_data.vertices.is_empty()
1360 {
1361 let expanded = self
1362 .wgpu_renderer
1363 .create_direct_point_vertices(&render_data.vertices, 0.0);
1364 let buffer = self.wgpu_renderer.create_vertex_buffer(&expanded);
1365 point_buffers.push(Some((buffer, expanded.len())));
1366 } else {
1367 point_buffers.push(None);
1368 }
1369 }
1370 self.wgpu_renderer.update_marker_screen_uniforms([
1371 config.width.max(1) as f32,
1372 config.height.max(1) as f32,
1373 ]);
1374
1375 {
1377 let depth_view = self.wgpu_renderer.ensure_depth_view();
1378 let use_msaa = self.wgpu_renderer.msaa_sample_count > 1;
1379 let mut cached_msaa_view: Option<Arc<wgpu::TextureView>> = None;
1380
1381 let (color_view, resolve_target) = if use_msaa {
1382 if let Some(explicit_resolve_target) = target.resolve_target {
1383 (target.view, Some(explicit_resolve_target))
1384 } else {
1385 cached_msaa_view = Some(self.wgpu_renderer.ensure_msaa_color_view());
1386 (
1387 cached_msaa_view
1388 .as_ref()
1389 .expect("msaa color view should exist")
1390 .as_ref(),
1391 Some(target.view),
1392 )
1393 }
1394 } else {
1395 (target.view, target.resolve_target)
1396 };
1397
1398 let depth_clear = match self.wgpu_renderer.depth_mode {
1399 crate::core::DepthMode::Standard => 1.0,
1400 crate::core::DepthMode::ReversedZ => 0.0,
1401 };
1402 let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1403 label: Some("Plot Render Pass"),
1404 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1405 view: color_view,
1406 resolve_target,
1407 ops: wgpu::Operations {
1408 load: wgpu::LoadOp::Clear(wgpu::Color {
1409 r: config.background_color.x as f64,
1410 g: config.background_color.y as f64,
1411 b: config.background_color.z as f64,
1412 a: config.background_color.w as f64,
1413 }),
1414 store: wgpu::StoreOp::Store,
1415 },
1416 })],
1417 depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
1418 view: &depth_view,
1419 depth_ops: Some(wgpu::Operations {
1420 load: wgpu::LoadOp::Clear(depth_clear),
1421 store: wgpu::StoreOp::Discard,
1422 }),
1423 stencil_ops: None,
1424 }),
1425 occlusion_query_set: None,
1426 timestamp_writes: None,
1427 });
1428 let _keep_msaa_view_alive = &cached_msaa_view;
1429
1430 for (i, (render_data, vertex_buffer, index_buffer)) in render_items.iter().enumerate() {
1432 #[cfg(target_arch = "wasm32")]
1433 {
1434 if log::log_enabled!(log::Level::Debug) {
1437 if let Some(v0) = render_data.vertices.first() {
1438 log::debug!(
1439 target: "runmat_plot",
1440 "wasm draw item: pipeline={:?} verts={} v0.pos=({:.3},{:.3},{:.3}) v0.color=({:.3},{:.3},{:.3},{:.3})",
1441 render_data.pipeline_type,
1442 render_data.vertices.len(),
1443 v0.position[0],
1444 v0.position[1],
1445 v0.position[2],
1446 v0.color[0],
1447 v0.color[1],
1448 v0.color[2],
1449 v0.color[3],
1450 );
1451 } else if render_data.gpu_vertices.is_some() {
1452 log::debug!(
1453 target: "runmat_plot",
1454 "wasm draw item: pipeline={:?} using gpu_vertices vertex_count={}",
1455 render_data.pipeline_type,
1456 render_data.vertex_count(),
1457 );
1458 } else {
1459 log::debug!(
1460 target: "runmat_plot",
1461 "wasm draw item: pipeline={:?} has no vertices",
1462 render_data.pipeline_type
1463 );
1464 }
1465 }
1466 }
1467
1468 if render_data.pipeline_type == crate::core::PipelineType::Textured {
1470 let pipeline = self.wgpu_renderer.get_pipeline(render_data.pipeline_type);
1472 render_pass.set_pipeline(pipeline);
1473 render_pass.set_bind_group(
1476 0,
1477 &self.wgpu_renderer.direct_uniform_bind_group,
1478 &[],
1479 );
1480 if let Some(ref img_bg) = image_bind_groups[i] {
1481 render_pass.set_bind_group(1, img_bg, &[]);
1482 }
1483 } else {
1484 let pipeline = self.wgpu_renderer.get_pipeline(render_data.pipeline_type);
1485 render_pass.set_pipeline(pipeline);
1486 render_pass.set_bind_group(0, self.wgpu_renderer.get_uniform_bind_group(), &[]);
1488 if matches!(
1489 render_data.pipeline_type,
1490 crate::core::PipelineType::Points | crate::core::PipelineType::Scatter3
1491 ) {
1492 if let Some(ref bg) = point_style_bind_groups[i] {
1493 render_pass.set_bind_group(1, bg, &[]);
1494 }
1495 render_pass.set_bind_group(
1496 2,
1497 self.wgpu_renderer.get_marker_screen_bind_group(),
1498 &[],
1499 );
1500 }
1501 }
1502
1503 let is_markers = matches!(
1504 render_data.pipeline_type,
1505 crate::core::PipelineType::Points | crate::core::PipelineType::Scatter3
1506 );
1507 if is_markers {
1508 if let Some((ref expanded, _len)) = point_buffers[i] {
1509 render_pass.set_vertex_buffer(0, expanded.slice(..));
1510 } else {
1511 render_pass.set_vertex_buffer(0, vertex_buffer.slice(..));
1512 }
1513 } else {
1514 render_pass.set_vertex_buffer(0, vertex_buffer.slice(..));
1515 }
1516
1517 if let Some(index_buffer) = index_buffer {
1518 render_pass.set_index_buffer(index_buffer.slice(..), wgpu::IndexFormat::Uint32);
1519 if let Some(indices) = &render_data.indices {
1520 log::trace!(target: "runmat_plot", "draw indexed count={}", indices.len());
1521 render_pass.draw_indexed(0..indices.len() as u32, 0, 0..1);
1522 }
1523 } else {
1524 log::trace!(target: "runmat_plot", "draw direct vertices");
1525 if let Some((args, offset)) = Self::gpu_indirect_args(render_data) {
1526 render_pass.draw_indirect(args, offset);
1527 continue;
1528 }
1529 if is_markers {
1530 if let Some((_, len)) = point_buffers[i] {
1531 render_pass.draw(0..len as u32, 0..1);
1532 continue;
1533 }
1534 }
1535 for draw_call in &render_data.draw_calls {
1537 log::trace!(
1538 target: "runmat_plot",
1539 "draw vertices offset={} count={} instances={}",
1540 draw_call.vertex_offset,
1541 draw_call.vertex_count,
1542 draw_call.instance_count
1543 );
1544 render_pass.draw(
1545 draw_call.vertex_offset as u32
1546 ..(draw_call.vertex_offset + draw_call.vertex_count) as u32,
1547 0..draw_call.instance_count as u32,
1548 );
1549 }
1550 }
1551 }
1552 }
1554
1555 let render_time = start_time.elapsed().as_secs_f64() * 1000.0;
1556
1557 Ok(RenderResult {
1558 success: true,
1559 data_bounds: self.data_bounds,
1560 vertex_count: total_vertices,
1561 triangle_count: total_triangles,
1562 render_time_ms: render_time,
1563 })
1564 }
1565
1566 pub fn render_scene_to_target(
1571 &mut self,
1572 encoder: &mut wgpu::CommandEncoder,
1573 target_view: &wgpu::TextureView,
1574 config: &PlotRenderConfig,
1575 ) -> Result<RenderResult, Box<dyn std::error::Error>> {
1576 let start_time = Instant::now();
1577 let (rows, cols) = self.figure_axes_grid();
1578 let axes_count = rows.saturating_mul(cols);
1579 log::debug!(
1580 "runmat-plot: renderer.scene_to_target.start rows={} cols={} axes_count={} width={} height={}",
1581 rows,
1582 cols,
1583 axes_count,
1584 config.width,
1585 config.height
1586 );
1587 if axes_count <= 1 {
1588 log::debug!("runmat-plot: renderer.scene_to_target.branch_single_axes");
1589 return self.render(
1590 encoder,
1591 RenderTarget {
1592 view: target_view,
1593 resolve_target: None,
1594 },
1595 config,
1596 );
1597 }
1598
1599 let viewports =
1600 Self::compute_tiled_viewports(config.width.max(1), config.height.max(1), rows, cols);
1601 log::debug!(
1602 "runmat-plot: renderer.scene_to_target.branch_subplot_axes viewports={}",
1603 viewports.len()
1604 );
1605 self.render_axes_to_viewports(
1606 encoder,
1607 target_view,
1608 &viewports,
1609 config.msaa_samples.max(1),
1610 config,
1611 )?;
1612 let stats = self.scene.statistics();
1613 Ok(RenderResult {
1614 success: true,
1615 data_bounds: self.data_bounds,
1616 vertex_count: stats.total_vertices,
1617 triangle_count: stats.total_triangles,
1618 render_time_ms: start_time.elapsed().as_secs_f64() * 1000.0,
1619 })
1620 }
1621
1622 fn compute_tiled_viewports(
1623 total_width: u32,
1624 total_height: u32,
1625 rows: usize,
1626 cols: usize,
1627 ) -> Vec<(u32, u32, u32, u32)> {
1628 if rows == 0 || cols == 0 {
1629 return vec![(0, 0, total_width.max(1), total_height.max(1))];
1630 }
1631 let rows_u32 = rows as u32;
1632 let cols_u32 = cols as u32;
1633 let cell_w = (total_width / cols_u32).max(1);
1634 let cell_h = (total_height / rows_u32).max(1);
1635 let mut out = Vec::with_capacity(rows * cols);
1636 for r in 0..rows_u32 {
1637 for c in 0..cols_u32 {
1638 let x = c * cell_w;
1639 let y = r * cell_h;
1640 let mut w = cell_w;
1641 let mut h = cell_h;
1642 if c + 1 == cols_u32 {
1643 w = total_width.saturating_sub(x).max(1);
1644 }
1645 if r + 1 == rows_u32 {
1646 h = total_height.saturating_sub(y).max(1);
1647 }
1648 out.push((x, y, w, h));
1649 }
1650 }
1651 out
1652 }
1653
1654 #[allow(clippy::too_many_arguments)]
1657 pub fn render_camera_to_viewport(
1658 &mut self,
1659 encoder: &mut wgpu::CommandEncoder,
1660 target_view: &wgpu::TextureView,
1661 viewport_scissor: (u32, u32, u32, u32),
1662 config: &PlotRenderConfig,
1663 camera: &Camera,
1664 axes_index: usize,
1665 clear_background: bool,
1666 ) -> Result<RenderResult, Box<dyn std::error::Error>> {
1667 log::debug!(
1668 "runmat-plot: renderer.camera_to_viewport.start axes_index={} viewport=({}, {}, {}, {}) clear_background={}",
1669 axes_index,
1670 viewport_scissor.0,
1671 viewport_scissor.1,
1672 viewport_scissor.2,
1673 viewport_scissor.3,
1674 clear_background
1675 );
1676 let use_msaa = config.msaa_samples.max(1) > 1;
1677 self.wgpu_renderer.ensure_msaa(config.msaa_samples);
1678 let msaa_view_keepalive = if use_msaa {
1679 Some(self.wgpu_renderer.ensure_msaa_color_view())
1680 } else {
1681 None
1682 };
1683 let render_target = if let Some(msaa_view) = msaa_view_keepalive.as_ref() {
1684 RenderTarget {
1685 view: msaa_view.as_ref(),
1686 resolve_target: Some(target_view),
1687 }
1688 } else {
1689 RenderTarget {
1690 view: target_view,
1691 resolve_target: None,
1692 }
1693 };
1694 self.render_camera_to_target_viewport(
1695 encoder,
1696 render_target,
1697 viewport_scissor,
1698 config,
1699 camera,
1700 axes_index,
1701 clear_background,
1702 )
1703 }
1704
1705 #[allow(clippy::too_many_arguments)]
1706 fn render_camera_to_target_viewport(
1707 &mut self,
1708 encoder: &mut wgpu::CommandEncoder,
1709 target: RenderTarget<'_>,
1710 viewport_scissor: (u32, u32, u32, u32),
1711 config: &PlotRenderConfig,
1712 camera: &Camera,
1713 axes_index: usize,
1714 clear_background: bool,
1715 ) -> Result<RenderResult, Box<dyn std::error::Error>> {
1716 let start_time = Instant::now();
1717
1718 self.wgpu_renderer.ensure_msaa(config.msaa_samples);
1720 self.wgpu_renderer.set_depth_mode(config.depth_mode);
1721
1722 let depth_view = self.wgpu_renderer.ensure_depth_view();
1726
1727 let aspect_ratio = (config.width.max(1)) as f32 / (config.height.max(1)) as f32;
1729 let mut cam = camera.clone();
1730 cam.update_aspect_ratio(aspect_ratio);
1731 cam.depth_mode = config.depth_mode;
1732 log::debug!(
1733 "runmat-plot: renderer.camera_to_target_viewport.camera_ready axes_index={} aspect_ratio={} msaa_samples={}",
1734 axes_index,
1735 aspect_ratio,
1736 config.msaa_samples
1737 );
1738
1739 if config.clip_policy.dynamic {
1742 let mut bounds: Option<crate::core::scene::BoundingBox> = None;
1743 for node in self.scene.get_visible_nodes() {
1744 if let Some(rd) = &node.render_data {
1745 if let Some(b) = rd.bounds {
1746 bounds = Some(bounds.map_or(b, |acc| acc.union(&b)));
1747 }
1748 }
1749 }
1750 if let Some(b) = bounds {
1751 cam.update_clip_planes_from_world_aabb(b.min, b.max, &config.clip_policy);
1752 }
1753 }
1754 let view_proj_matrix = cam.view_proj_matrix();
1755 let model_matrix = self.axes_model_matrix(axes_index);
1756 self.wgpu_renderer
1757 .update_uniforms_for_axes(axes_index, view_proj_matrix, model_matrix);
1758 log::debug!(
1759 "runmat-plot: renderer.camera_to_target_viewport.uniforms_updated axes_index={}",
1760 axes_index
1761 );
1762
1763 let (mut sx, mut sy, mut sw, mut sh) = viewport_scissor;
1764 let target_w = self.wgpu_renderer.surface_config.width.max(1);
1765 let target_h = self.wgpu_renderer.surface_config.height.max(1);
1766 if sx >= target_w || sy >= target_h {
1767 return Ok(RenderResult {
1768 success: true,
1769 data_bounds: self.data_bounds,
1770 vertex_count: 0,
1771 triangle_count: 0,
1772 render_time_ms: 0.0,
1773 });
1774 }
1775 sx = sx.min(target_w.saturating_sub(1));
1776 sy = sy.min(target_h.saturating_sub(1));
1777 sw = sw.max(1).min(target_w.saturating_sub(sx).max(1));
1778 sh = sh.max(1).min(target_h.saturating_sub(sy).max(1));
1779 let is_2d = matches!(
1780 cam.projection,
1781 crate::core::camera::ProjectionType::Orthographic { .. }
1782 );
1783 log::debug!(
1784 "runmat-plot: renderer.camera_to_target_viewport.viewport_normalized axes_index={} viewport=({}, {}, {}, {}) is_2d={}",
1785 axes_index,
1786 sx,
1787 sy,
1788 sw,
1789 sh,
1790 is_2d
1791 );
1792 match cam.projection {
1793 crate::core::camera::ProjectionType::Orthographic {
1794 left,
1795 right,
1796 bottom,
1797 top,
1798 ..
1799 } => {
1800 log::debug!(
1801 target: "runmat_plot.draw_camera",
1802 "draw camera axes_index={} is_2d=true viewport=({}, {}, {}, {}) bounds=({}, {})..({}, {}) cfg_wh=({}, {})",
1803 axes_index,
1804 sx,
1805 sy,
1806 sw,
1807 sh,
1808 left,
1809 bottom,
1810 right,
1811 top,
1812 config.width,
1813 config.height
1814 );
1815 }
1816 crate::core::camera::ProjectionType::Perspective { .. } => {
1817 log::debug!(
1818 target: "runmat_plot.draw_camera",
1819 "draw camera axes_index={} is_2d=false viewport=({}, {}, {}, {}) cfg_wh=({}, {})",
1820 axes_index,
1821 sx,
1822 sy,
1823 sw,
1824 sh,
1825 config.width,
1826 config.height
1827 );
1828 }
1829 }
1830
1831 let mut owned_render_data: Vec<Box<crate::core::RenderData>> = Vec::new();
1833 let mut render_items = Vec::new();
1834 let mut grid_plane_buffers: Option<(wgpu::Buffer, wgpu::Buffer)> = None;
1835 let mut total_vertices = 0usize;
1836 let mut total_triangles = 0usize;
1837 log::debug!(
1838 "runmat-plot: renderer.camera_to_target_viewport.collect_render_items.start axes_index={}",
1839 axes_index
1840 );
1841 for node in self.scene.get_visible_nodes() {
1842 if let Some(render_data) = &node.render_data {
1843 if node.axes_index == axes_index {
1844 log::debug!(
1845 target: "runmat_plot.draw_item",
1846 "draw item axes_index={} node_axes_index={} pipeline={:?} vertex_count={} has_indices={} has_bounds={} gpu_vertices={}",
1847 axes_index,
1848 node.axes_index,
1849 render_data.pipeline_type,
1850 render_data.vertex_count(),
1851 render_data.indices.is_some(),
1852 render_data.bounds.is_some(),
1853 render_data.gpu_vertices.is_some()
1854 );
1855 }
1856 if let Some((vb, ib)) = self.prepare_buffers_for_render_data(node.id, render_data) {
1857 self.wgpu_renderer
1858 .ensure_pipeline(render_data.pipeline_type);
1859 total_vertices += render_data.vertex_count();
1860 if let Some(indices) = &render_data.indices {
1861 total_triangles += indices.len() / 3;
1862 }
1863 render_items.push((render_data, vb, ib));
1864 }
1865 }
1866 }
1867 log::debug!(
1868 "runmat-plot: renderer.camera_to_target_viewport.collect_render_items.ok axes_index={} items={} total_vertices={} total_triangles={}",
1869 axes_index,
1870 render_items.len(),
1871 total_vertices,
1872 total_triangles
1873 );
1874
1875 if !is_2d {
1878 let view_proj = view_proj_matrix;
1879 let inv_view_proj = view_proj.inverse();
1880
1881 let unproject = |ndc_x: f32, ndc_y: f32, ndc_z: f32| -> Option<Vec3> {
1882 let clip = Vec4::new(ndc_x, ndc_y, ndc_z, 1.0);
1883 let world = inv_view_proj * clip;
1884 if !world.w.is_finite() || world.w.abs() < 1e-6 {
1885 return None;
1886 }
1887 let p = world.truncate() / world.w;
1888 if p.x.is_finite() && p.y.is_finite() && p.z.is_finite() {
1889 Some(p)
1890 } else {
1891 None
1892 }
1893 };
1894
1895 let ray_intersect_z0 = |ndc_x: f32, ndc_y: f32| -> Option<Vec3> {
1896 let p0 = unproject(ndc_x, ndc_y, -1.0)?;
1898 let p1 = unproject(ndc_x, ndc_y, 1.0)?;
1899 let dir = p1 - p0;
1900 if !dir.z.is_finite() || dir.z.abs() < 1e-8 {
1901 return None;
1902 }
1903 let t = (-p0.z) / dir.z;
1904 if !t.is_finite() || t <= 0.0 {
1905 return None;
1906 }
1907 Some(p0 + dir * t)
1908 };
1909
1910 let mut plane_pts: Vec<Vec3> = Vec::new();
1911 for (nx, ny) in [(-1.0, -1.0), (1.0, -1.0), (1.0, 1.0), (-1.0, 1.0)] {
1912 if let Some(p) = ray_intersect_z0(nx, ny) {
1913 plane_pts.push(p);
1914 }
1915 }
1916
1917 let mut min_x = 0.0_f32;
1919 let mut max_x = 1.0_f32;
1920 let mut min_y = 0.0_f32;
1921 let mut max_y = 1.0_f32;
1922
1923 if plane_pts.len() >= 2 {
1924 min_x = plane_pts.iter().map(|p| p.x).fold(f32::INFINITY, f32::min);
1925 max_x = plane_pts
1926 .iter()
1927 .map(|p| p.x)
1928 .fold(f32::NEG_INFINITY, f32::max);
1929 min_y = plane_pts.iter().map(|p| p.y).fold(f32::INFINITY, f32::min);
1930 max_y = plane_pts
1931 .iter()
1932 .map(|p| p.y)
1933 .fold(f32::NEG_INFINITY, f32::max);
1934 } else if let crate::core::camera::ProjectionType::Perspective { fov, .. } =
1935 cam.projection
1936 {
1937 let dist = (cam.position - cam.target).length().max(1e-3);
1938 let extent = (dist * (0.5 * fov).tan() * 1.25).max(0.5);
1939 let center = Vec3::new(cam.target.x, cam.target.y, 0.0);
1940 min_x = center.x - extent;
1941 max_x = center.x + extent;
1942 min_y = center.y - extent;
1943 max_y = center.y + extent;
1944 }
1945
1946 let dx = (max_x - min_x).abs().max(1e-3);
1948 let dy = (max_y - min_y).abs().max(1e-3);
1949 let margin_x = dx * 0.04;
1950 let margin_y = dy * 0.04;
1951 min_x -= margin_x;
1952 max_x += margin_x;
1953 min_y -= margin_y;
1954 max_y += margin_y;
1955
1956 let project_to_px = |p: Vec3| -> Option<(f32, f32)> {
1957 let clip = view_proj * Vec4::new(p.x, p.y, p.z, 1.0);
1958 if !clip.w.is_finite() || clip.w.abs() < 1e-6 {
1959 return None;
1960 }
1961 let ndc = clip.truncate() / clip.w;
1962 if !(ndc.x.is_finite() && ndc.y.is_finite()) {
1963 return None;
1964 }
1965 let px = ((ndc.x + 1.0) * 0.5) * (sw.max(1) as f32);
1966 let py = ((1.0 - ndc.y) * 0.5) * (sh.max(1) as f32);
1967 Some((px, py))
1968 };
1969
1970 let nice_step = |raw: f64| -> f64 {
1971 if !raw.is_finite() || raw <= 0.0 {
1972 return 1.0;
1973 }
1974 let pow10 = 10.0_f64.powf(raw.log10().floor());
1975 let norm = raw / pow10;
1976 let mult = if norm <= 1.0 {
1977 1.0
1978 } else if norm <= 2.0 {
1979 2.0
1980 } else if norm <= 5.0 {
1981 5.0
1982 } else {
1983 10.0
1984 };
1985 mult * pow10
1986 };
1987
1988 let cx = (min_x + max_x) * 0.5;
1990 let cy = (min_y + max_y) * 0.5;
1991 let center = Vec3::new(cx, cy, 0.0);
1992 let px_per_world = {
1993 let a = project_to_px(center);
1994 let b = project_to_px(center + Vec3::new(1.0, 0.0, 0.0));
1995 match (a, b) {
1996 (Some((ax, ay)), Some((bx, by))) => ((bx - ax).hypot(by - ay)).max(1e-3),
1997 _ => 1.0,
1998 }
1999 };
2000 let desired_major_px = 120.0_f64;
2001 let major_step = nice_step((desired_major_px / (px_per_world as f64)).max(1e-6));
2002 let mut minor_step = major_step / 10.0;
2003 if !minor_step.is_finite() || minor_step <= 0.0 {
2004 minor_step = major_step.max(1.0);
2005 }
2006
2007 let max_minor_lines = 180.0;
2009 let minor_count_x = (dx as f64 / minor_step).abs();
2010 let minor_count_y = (dy as f64 / minor_step).abs();
2011 if minor_count_x > max_minor_lines || minor_count_y > max_minor_lines {
2012 minor_step = (major_step / 5.0).max(major_step); }
2014
2015 let mut helper_vertices: Vec<Vertex> = Vec::new();
2016 let mut push_line = |a: Vec3, b: Vec3, color: Vec4| {
2017 helper_vertices.push(Vertex::new(a, color));
2018 helper_vertices.push(Vertex::new(b, color));
2019 };
2020
2021 let z_grid = -1e-4_f32;
2023
2024 let show_major_grid = self.overlay_show_grid_for_axes(axes_index);
2027 let show_minor_grid = self.overlay_show_minor_grid_for_axes(axes_index);
2028 if show_major_grid || show_minor_grid {
2029 let theme = self.theme.build_theme();
2030 let bg = theme.get_background_color();
2031 let grid = theme.get_grid_color();
2032 let bg_luma = 0.2126 * bg.x + 0.7152 * bg.y + 0.0722 * bg.z;
2033 let mut major_rgb = [grid.x, grid.y, grid.z];
2034 let mut minor_rgb = [grid.x, grid.y, grid.z];
2035 let mut major_alpha = grid.w.clamp(0.08, 0.22);
2036 let mut minor_alpha = (grid.w * 0.45).clamp(0.04, 0.14);
2037 if bg_luma <= 0.62 {
2038 major_rgb = [grid.x * 0.80, grid.y * 0.80, grid.z * 0.80];
2039 minor_rgb = [grid.x * 0.68, grid.y * 0.68, grid.z * 0.68];
2040 }
2041 if bg_luma > 0.62 {
2042 major_rgb = [grid.x * 0.45, grid.y * 0.45, grid.z * 0.45];
2043 minor_rgb = [grid.x * 0.33, grid.y * 0.33, grid.z * 0.33];
2044 major_alpha = major_alpha.max(0.24);
2045 minor_alpha = minor_alpha.max(0.12);
2046 }
2047 if !show_major_grid {
2048 major_alpha = 0.0;
2049 }
2050 if !show_minor_grid {
2051 minor_alpha = 0.0;
2052 }
2053 self.wgpu_renderer.ensure_grid_plane_pipeline();
2054 self.wgpu_renderer.update_grid_uniforms_for_axes(
2055 axes_index,
2056 crate::core::renderer::GridUniforms {
2057 major_step: major_step as f32,
2058 minor_step: minor_step as f32,
2059 fade_start: (0.60 * dx.max(dy)).max(major_step as f32),
2060 fade_end: (0.95 * dx.max(dy)).max((major_step as f32) * 2.0),
2061 camera_pos: cam.position.to_array(),
2062 _pad0: 0.0,
2063 target_pos: Vec3::new(cam.target.x, cam.target.y, 0.0).to_array(),
2064 _pad1: 0.0,
2065 major_color: [major_rgb[0], major_rgb[1], major_rgb[2], major_alpha],
2066 minor_color: [minor_rgb[0], minor_rgb[1], minor_rgb[2], minor_alpha],
2067 },
2068 );
2069
2070 let quad_vertices = [
2071 Vertex::new(Vec3::new(min_x, min_y, z_grid), Vec4::ONE),
2072 Vertex::new(Vec3::new(max_x, min_y, z_grid), Vec4::ONE),
2073 Vertex::new(Vec3::new(max_x, max_y, z_grid), Vec4::ONE),
2074 Vertex::new(Vec3::new(min_x, max_y, z_grid), Vec4::ONE),
2075 ];
2076 let quad_indices: [u32; 6] = [0, 1, 2, 0, 2, 3];
2077 let vb = self.wgpu_renderer.create_vertex_buffer(&quad_vertices);
2078 let ib = self.wgpu_renderer.create_index_buffer(&quad_indices);
2079 grid_plane_buffers = Some((vb, ib));
2080 }
2081
2082 let axis_len = (major_step as f32 * 5.0).clamp(0.5, (dx.max(dy) * 0.6).max(0.5));
2084 let origin = Vec3::new(0.0, 0.0, 0.0);
2085 let col_x = Vec4::new(0.92, 0.25, 0.25, 0.85);
2086 let col_y = Vec4::new(0.35, 0.90, 0.45, 0.85);
2087 let col_z = Vec4::new(0.35, 0.62, 0.98, 0.85);
2088 push_line(origin, origin + Vec3::new(axis_len, 0.0, 0.0), col_x);
2089 push_line(origin, origin + Vec3::new(0.0, axis_len, 0.0), col_y);
2090 push_line(origin, origin + Vec3::new(0.0, 0.0, axis_len), col_z);
2091
2092 let tick_max = (major_step as f32 * 0.25).max(1.0e-6);
2098 let tick_min = 0.01_f32.min(tick_max);
2099 let tick_len = (axis_len * 0.04).clamp(tick_min, tick_max);
2100 let max_ticks = 6usize;
2101 let mut add_ticks = |axis: Vec3, perp: Vec3, col: Vec4| {
2102 if major_step <= 0.0 {
2103 return;
2104 }
2105 for i in 1..=max_ticks {
2106 let t = (i as f32) * (major_step as f32);
2107 if t >= axis_len * 0.999 {
2108 break;
2109 }
2110 let p = origin + axis * t;
2111 push_line(
2112 p - perp * tick_len,
2113 p + perp * tick_len,
2114 Vec4::new(col.x, col.y, col.z, col.w * 0.85),
2115 );
2116 }
2117 };
2118 add_ticks(Vec3::X, Vec3::Y, col_x);
2119 add_ticks(Vec3::Y, Vec3::X, col_y);
2120 add_ticks(Vec3::Z, Vec3::X, col_z);
2121
2122 if !helper_vertices.is_empty() {
2123 let rd = Box::new(crate::core::RenderData {
2124 pipeline_type: crate::core::PipelineType::Lines,
2125 vertices: helper_vertices,
2126 indices: None,
2127 gpu_vertices: None,
2128 bounds: None,
2129 material: crate::core::Material::default(),
2130 draw_calls: vec![crate::core::DrawCall {
2131 vertex_offset: 0,
2132 vertex_count: 0, index_offset: None,
2134 index_count: None,
2135 instance_count: 1,
2136 }],
2137 image: None,
2138 });
2139 owned_render_data.push(rd);
2140 let idx = owned_render_data.len() - 1;
2141 let vcount = owned_render_data[idx].vertices.len();
2143 if let Some(dc) = owned_render_data[idx].draw_calls.get_mut(0) {
2144 dc.vertex_count = vcount;
2145 }
2146 let vb = Arc::new(
2147 self.wgpu_renderer
2148 .create_vertex_buffer(&owned_render_data[idx].vertices),
2149 );
2150 let rd_ref: &crate::core::RenderData = &owned_render_data[idx];
2152 render_items.insert(0, (rd_ref, vb, None));
2153 total_vertices += vcount;
2154 }
2155 }
2156
2157 let mut point_buffers: Vec<Option<(wgpu::Buffer, usize)>> =
2159 Vec::with_capacity(render_items.len());
2160 for (render_data, _vb, _ib) in render_items.iter() {
2161 if matches!(
2162 render_data.pipeline_type,
2163 crate::core::PipelineType::Points | crate::core::PipelineType::Scatter3
2164 ) && !render_data.vertices.is_empty()
2165 {
2166 let expanded = self
2167 .wgpu_renderer
2168 .create_direct_point_vertices(&render_data.vertices, 0.0);
2170 let buf = self.wgpu_renderer.create_vertex_buffer(&expanded);
2171 point_buffers.push(Some((buf, expanded.len())));
2172 } else {
2173 point_buffers.push(None);
2174 }
2175 }
2176 let has_textured_items = render_items.iter().any(|(render_data, _vb, _ib)| {
2178 render_data.pipeline_type == crate::core::PipelineType::Textured
2179 });
2180 if has_textured_items {
2181 self.wgpu_renderer.ensure_image_pipeline();
2182 }
2183 let mut image_bind_groups: Vec<Option<wgpu::BindGroup>> =
2184 Vec::with_capacity(render_items.len());
2185
2186 for (render_data, _vb, _ib) in render_items.iter() {
2187 if render_data.pipeline_type == crate::core::PipelineType::Textured {
2188 if let Some(crate::core::scene::ImageData::Rgba8 {
2189 width,
2190 height,
2191 data,
2192 }) = &render_data.image
2193 {
2194 let (_t, _v, bg) = self
2195 .wgpu_renderer
2196 .create_image_texture_and_bind_group(*width, *height, data);
2197 image_bind_groups.push(Some(bg));
2198 } else {
2199 image_bind_groups.push(None);
2200 }
2201 } else {
2202 image_bind_groups.push(None);
2203 }
2204 }
2205 let mut point_style_bind_groups: Vec<Option<wgpu::BindGroup>> =
2207 Vec::with_capacity(render_items.len());
2208 for (render_data, _vb, _ib) in render_items.iter() {
2209 if matches!(
2210 render_data.pipeline_type,
2211 crate::core::PipelineType::Points | crate::core::PipelineType::Scatter3
2212 ) {
2213 let style = crate::core::renderer::PointStyleUniforms {
2214 face_color: render_data.material.albedo.to_array(),
2215 edge_color: render_data.material.emissive.to_array(),
2216 edge_thickness_px: render_data.material.roughness,
2217 marker_shape: render_data.material.metallic as u32,
2218 _pad: [0.0, 0.0],
2219 };
2220 let (_buf, bg) = self.wgpu_renderer.create_point_style_bind_group(style);
2221 point_style_bind_groups.push(Some(bg));
2222 } else {
2223 point_style_bind_groups.push(None);
2224 }
2225 }
2226
2227 let mut grid_vb_opt: Option<wgpu::Buffer> = None;
2230 let show_major_grid = self.overlay_show_grid_for_axes(axes_index);
2231 let show_minor_grid = self.overlay_show_minor_grid_for_axes(axes_index);
2232 if is_2d && (show_major_grid || show_minor_grid) {
2233 if let Some((l, r, b, t)) = self.view_bounds_for_axes(axes_index) {
2234 self.wgpu_renderer.update_direct_uniforms_for_axes(
2236 axes_index,
2237 [l as f32, b as f32],
2238 [r as f32, t as f32],
2239 [-1.0, -1.0],
2240 [1.0, 1.0],
2241 [sw.max(1) as f32, sh.max(1) as f32],
2242 );
2243 self.wgpu_renderer.ensure_direct_line_pipeline();
2244
2245 let x_range = (r - l).max(1e-6);
2246 let y_range = (t - b).max(1e-6);
2247 let x_step = plot_utils::calculate_tick_interval(x_range);
2248 let y_step = plot_utils::calculate_tick_interval(y_range);
2249 let mut grid_vertices: Vec<Vertex> = Vec::new();
2250 let g = 80.0_f32 / 255.0_f32;
2251 let major_col = Vec4::new(g, g, g, 1.0);
2252 let minor_col = Vec4::new(g, g, g, 0.42);
2253 if show_minor_grid && x_step.is_finite() && x_step > 0.0 {
2254 let minor_step = (x_step / 5.0).max(f64::EPSILON);
2255 Self::push_vertical_grid_lines(
2256 &mut grid_vertices,
2257 (l, r),
2258 (b, t),
2259 minor_step,
2260 minor_col,
2261 Some((x_step, minor_step * 0.25)),
2262 );
2263 }
2264 if show_minor_grid && y_step.is_finite() && y_step > 0.0 {
2265 let minor_step = (y_step / 5.0).max(f64::EPSILON);
2266 Self::push_horizontal_grid_lines(
2267 &mut grid_vertices,
2268 (b, t),
2269 (l, r),
2270 minor_step,
2271 minor_col,
2272 Some((y_step, minor_step * 0.25)),
2273 );
2274 }
2275 if show_major_grid && x_step.is_finite() && x_step > 0.0 {
2276 Self::push_vertical_grid_lines(
2277 &mut grid_vertices,
2278 (l, r),
2279 (b, t),
2280 x_step,
2281 major_col,
2282 None,
2283 );
2284 }
2285 if show_major_grid && y_step.is_finite() && y_step > 0.0 {
2286 Self::push_horizontal_grid_lines(
2287 &mut grid_vertices,
2288 (b, t),
2289 (l, r),
2290 y_step,
2291 major_col,
2292 None,
2293 );
2294 }
2295 if !grid_vertices.is_empty() {
2296 grid_vb_opt = Some(self.wgpu_renderer.create_vertex_buffer(&grid_vertices));
2297 }
2298 }
2299 }
2300
2301 let bounds_opt = if is_2d {
2303 match cam.projection {
2304 crate::core::camera::ProjectionType::Orthographic {
2305 left,
2306 right,
2307 bottom,
2308 top,
2309 ..
2310 } => Some((left as f64, right as f64, bottom as f64, top as f64)),
2311 _ => self.data_bounds,
2312 }
2313 } else {
2314 None
2315 };
2316 if is_2d {
2317 if let Some((l, r, b, t)) = bounds_opt {
2318 self.wgpu_renderer.update_direct_uniforms_for_axes(
2319 axes_index,
2320 [l as f32, b as f32],
2321 [r as f32, t as f32],
2322 [-1.0, -1.0],
2323 [1.0, 1.0],
2324 [sw.max(1) as f32, sh.max(1) as f32],
2325 );
2326 }
2327 self.wgpu_renderer.ensure_direct_triangle_pipeline();
2328 self.wgpu_renderer.ensure_direct_line_pipeline();
2329 self.wgpu_renderer.ensure_direct_point_pipeline();
2330 } else {
2331 self.wgpu_renderer
2333 .ensure_pipeline(crate::core::PipelineType::Triangles);
2334 self.wgpu_renderer
2335 .ensure_pipeline(crate::core::PipelineType::Lines);
2336 self.wgpu_renderer
2337 .ensure_pipeline(crate::core::PipelineType::Points);
2338 }
2339 self.wgpu_renderer.update_marker_screen_uniforms_for_axes(
2340 axes_index,
2341 [sw.max(1) as f32, sh.max(1) as f32],
2342 );
2343
2344 {
2346 let use_msaa = self.wgpu_renderer.msaa_sample_count > 1;
2348 log::debug!(
2349 "runmat-plot: renderer.camera_to_target_viewport.render_pass_start axes_index={} use_msaa={} clear_background={}",
2350 axes_index,
2351 use_msaa,
2352 clear_background
2353 );
2354
2355 let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
2356 label: Some("Plot Camera Viewport Pass"),
2357 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
2358 view: target.view,
2359 resolve_target: if use_msaa {
2360 target.resolve_target
2361 } else {
2362 None
2363 },
2364 ops: wgpu::Operations {
2365 load: if clear_background {
2366 wgpu::LoadOp::Clear(wgpu::Color {
2367 r: config.background_color.x as f64,
2368 g: config.background_color.y as f64,
2369 b: config.background_color.z as f64,
2370 a: config.background_color.w as f64,
2371 })
2372 } else {
2373 wgpu::LoadOp::Load
2374 },
2375 store: wgpu::StoreOp::Store,
2376 },
2377 })],
2378 depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
2379 view: depth_view.as_ref(),
2380 depth_ops: Some(wgpu::Operations {
2381 load: wgpu::LoadOp::Clear(match config.depth_mode {
2382 DepthMode::Standard => 1.0,
2383 DepthMode::ReversedZ => 0.0,
2384 }),
2385 store: wgpu::StoreOp::Store,
2386 }),
2387 stencil_ops: None,
2388 }),
2389 timestamp_writes: None,
2390 occlusion_query_set: None,
2391 });
2392
2393 render_pass.set_viewport(
2395 sx as f32,
2396 sy as f32,
2397 sw.max(1) as f32,
2398 sh.max(1) as f32,
2399 0.0,
2400 1.0,
2401 );
2402 render_pass.set_scissor_rect(sx, sy, sw.max(1), sh.max(1));
2403 log::debug!(
2404 "runmat-plot: renderer.camera_to_target_viewport.render_pass_ready axes_index={} viewport=({}, {}, {}, {})",
2405 axes_index,
2406 sx,
2407 sy,
2408 sw.max(1),
2409 sh.max(1)
2410 );
2411 if let Some(ref vb_grid) = grid_vb_opt {
2412 if let Some(ref pipeline) = self.wgpu_renderer.direct_line_pipeline {
2413 log::debug!(
2414 "runmat-plot: renderer.camera_to_target_viewport.draw_grid_start axes_index={} vertex_buffer_size={}",
2415 axes_index,
2416 vb_grid.size()
2417 );
2418 render_pass.set_pipeline(pipeline);
2419 render_pass.set_bind_group(
2420 0,
2421 self.wgpu_renderer
2422 .get_direct_uniform_bind_group_for_axes(axes_index),
2423 &[],
2424 );
2425 render_pass.set_vertex_buffer(0, vb_grid.slice(..));
2426 render_pass.draw(
2431 0..(vb_grid.size() / std::mem::size_of::<Vertex>() as u64) as u32,
2432 0..1,
2433 );
2434 log::debug!(
2435 "runmat-plot: renderer.camera_to_target_viewport.draw_grid_ok axes_index={}",
2436 axes_index
2437 );
2438 }
2439 }
2440
2441 let use_direct_for_triangles = is_2d;
2443 let use_direct_for_lines = is_2d;
2444 let direct_tri_pipeline = if use_direct_for_triangles && bounds_opt.is_some() {
2445 self.wgpu_renderer
2446 .direct_triangle_pipeline
2447 .as_ref()
2448 .map(|p| p as *const wgpu::RenderPipeline)
2449 } else {
2450 None
2451 };
2452 let direct_line_pipeline = if use_direct_for_lines && bounds_opt.is_some() {
2453 self.wgpu_renderer
2454 .direct_line_pipeline
2455 .as_ref()
2456 .map(|p| p as *const wgpu::RenderPipeline)
2457 } else {
2458 None
2459 };
2460 let direct_point_pipeline = if is_2d && bounds_opt.is_some() {
2461 self.wgpu_renderer
2462 .direct_point_pipeline
2463 .as_ref()
2464 .map(|p| p as *const wgpu::RenderPipeline)
2465 } else {
2466 None
2467 };
2468
2469 for (idx, (render_data, vertex_buffer, index_buffer)) in render_items.iter().enumerate()
2470 {
2471 let is_triangles = matches!(
2472 render_data.pipeline_type,
2473 crate::core::PipelineType::Triangles
2474 );
2475 let is_lines =
2476 matches!(render_data.pipeline_type, crate::core::PipelineType::Lines);
2477 let is_points = matches!(
2478 render_data.pipeline_type,
2479 crate::core::PipelineType::Points | crate::core::PipelineType::Scatter3
2480 );
2481 let is_textured = matches!(
2482 render_data.pipeline_type,
2483 crate::core::PipelineType::Textured
2484 );
2485 let use_direct = is_2d
2487 && ((use_direct_for_triangles && is_triangles)
2488 || (use_direct_for_lines && is_lines)
2489 || is_points)
2490 && bounds_opt.is_some();
2491 log::debug!(
2492 "runmat-plot: renderer.camera_to_target_viewport.draw_item_start axes_index={} item_index={} pipeline={:?} use_direct={} textured={} indexed={} draw_calls={} point_buffer={} ",
2493 axes_index,
2494 idx,
2495 render_data.pipeline_type,
2496 use_direct,
2497 is_textured,
2498 index_buffer.is_some(),
2499 render_data.draw_calls.len(),
2500 point_buffers[idx].is_some()
2501 );
2502
2503 if use_direct {
2504 let pipeline_ref: &wgpu::RenderPipeline = unsafe {
2506 if is_triangles {
2507 direct_tri_pipeline.unwrap().as_ref().unwrap()
2508 } else if is_lines {
2509 direct_line_pipeline.unwrap().as_ref().unwrap()
2510 } else {
2511 direct_point_pipeline.unwrap().as_ref().unwrap()
2512 }
2513 };
2514 let uniform_bg = self
2515 .wgpu_renderer
2516 .get_direct_uniform_bind_group_for_axes(axes_index);
2517 render_pass.set_pipeline(pipeline_ref);
2518 render_pass.set_bind_group(0, uniform_bg, &[]);
2519 if is_points {
2520 if let Some(ref bg) = point_style_bind_groups[idx] {
2521 render_pass.set_bind_group(1, bg, &[]);
2522 }
2523 }
2524 log::debug!(
2525 "runmat-plot: renderer.camera_to_target_viewport.draw_item_pipeline_ready axes_index={} item_index={} branch=direct",
2526 axes_index,
2527 idx
2528 );
2529 } else if is_textured {
2530 let pipeline = self
2531 .wgpu_renderer
2532 .get_pipeline(crate::core::PipelineType::Textured);
2533 render_pass.set_pipeline(pipeline);
2534 render_pass.set_bind_group(
2535 0,
2536 self.wgpu_renderer
2537 .get_direct_uniform_bind_group_for_axes(axes_index),
2538 &[],
2539 );
2540 if let Some(ref bg) = image_bind_groups[idx] {
2541 render_pass.set_bind_group(1, bg, &[]);
2542 }
2543 log::debug!(
2544 "runmat-plot: renderer.camera_to_target_viewport.draw_item_pipeline_ready axes_index={} item_index={} branch=textured",
2545 axes_index,
2546 idx
2547 );
2548 } else {
2549 let pipeline = self.wgpu_renderer.get_pipeline(render_data.pipeline_type);
2550 render_pass.set_pipeline(pipeline);
2551 render_pass.set_bind_group(
2552 0,
2553 self.wgpu_renderer
2554 .get_uniform_bind_group_for_axes(axes_index),
2555 &[],
2556 );
2557 if is_points {
2558 if let Some(ref bg) = point_style_bind_groups[idx] {
2559 render_pass.set_bind_group(1, bg, &[]);
2560 }
2561 render_pass.set_bind_group(
2562 2,
2563 self.wgpu_renderer
2564 .get_marker_screen_bind_group_for_axes(axes_index),
2565 &[],
2566 );
2567 }
2568 log::debug!(
2569 "runmat-plot: renderer.camera_to_target_viewport.draw_item_pipeline_ready axes_index={} item_index={} branch=standard",
2570 axes_index,
2571 idx
2572 );
2573 }
2574
2575 if is_points && use_direct {
2576 if let Some((ref buf, len)) = point_buffers[idx] {
2577 render_pass.set_vertex_buffer(0, buf.slice(..));
2578 render_pass.draw(0..len as u32, 0..1);
2579 log::debug!(
2580 "runmat-plot: renderer.camera_to_target_viewport.draw_item_ok axes_index={} item_index={} mode=direct_points vertices={}",
2581 axes_index,
2582 idx,
2583 len
2584 );
2585 continue;
2586 }
2587 } else if is_points {
2588 if let Some((ref buf, _len)) = point_buffers[idx] {
2589 render_pass.set_vertex_buffer(0, buf.slice(..));
2590 } else {
2591 render_pass.set_vertex_buffer(0, vertex_buffer.slice(..));
2592 }
2593 } else {
2594 render_pass.set_vertex_buffer(0, vertex_buffer.slice(..));
2595 }
2596 if let Some(index_buffer_ref) = index_buffer {
2597 render_pass
2598 .set_index_buffer(index_buffer_ref.slice(..), wgpu::IndexFormat::Uint32);
2599 if let Some(indices) = &render_data.indices {
2600 render_pass.draw_indexed(0..indices.len() as u32, 0, 0..1);
2601 log::debug!(
2602 "runmat-plot: renderer.camera_to_target_viewport.draw_item_ok axes_index={} item_index={} mode=indexed indices={}",
2603 axes_index,
2604 idx,
2605 indices.len()
2606 );
2607 }
2608 } else {
2609 if is_points {
2610 if let Some((_, len)) = point_buffers[idx] {
2611 render_pass.draw(0..len as u32, 0..1);
2612 continue;
2613 }
2614 }
2615 for dc in &render_data.draw_calls {
2616 render_pass.draw(
2617 dc.vertex_offset as u32..(dc.vertex_offset + dc.vertex_count) as u32,
2618 0..dc.instance_count as u32,
2619 );
2620 log::debug!(
2621 "runmat-plot: renderer.camera_to_target_viewport.draw_call_ok axes_index={} item_index={} mode=draw vertex_offset={} vertex_count={} instances={}",
2622 axes_index,
2623 idx,
2624 dc.vertex_offset,
2625 dc.vertex_count,
2626 dc.instance_count
2627 );
2628 }
2629 }
2630 }
2631
2632 if let Some((ref vb, ref ib)) = grid_plane_buffers {
2634 if let Some(pipeline) = self.wgpu_renderer.grid_plane_pipeline() {
2635 log::debug!(
2636 "runmat-plot: renderer.camera_to_target_viewport.draw_grid_plane_start axes_index={}",
2637 axes_index
2638 );
2639 render_pass.set_pipeline(pipeline);
2640 render_pass.set_bind_group(
2641 0,
2642 self.wgpu_renderer
2643 .get_uniform_bind_group_for_axes(axes_index),
2644 &[],
2645 );
2646 render_pass.set_bind_group(
2647 1,
2648 self.wgpu_renderer
2649 .get_grid_uniform_bind_group_for_axes(axes_index),
2650 &[],
2651 );
2652 render_pass.set_vertex_buffer(0, vb.slice(..));
2653 render_pass.set_index_buffer(ib.slice(..), wgpu::IndexFormat::Uint32);
2654 render_pass.draw_indexed(0..6, 0, 0..1);
2655 log::debug!(
2656 "runmat-plot: renderer.camera_to_target_viewport.draw_grid_plane_ok axes_index={}",
2657 axes_index
2658 );
2659 }
2660 }
2661 }
2662
2663 log::debug!(
2664 "runmat-plot: renderer.camera_to_target_viewport.ok axes_index={} total_vertices={} total_triangles={}",
2665 axes_index,
2666 total_vertices,
2667 total_triangles
2668 );
2669
2670 Ok(RenderResult {
2671 success: true,
2672 data_bounds: self.data_bounds,
2673 vertex_count: total_vertices,
2674 triangle_count: total_triangles,
2675 render_time_ms: start_time.elapsed().as_secs_f64() * 1000.0,
2676 })
2677 }
2678
2679 pub fn render_axes_to_viewports(
2682 &mut self,
2683 encoder: &mut wgpu::CommandEncoder,
2684 target_view: &wgpu::TextureView,
2685 axes_viewports: &[(u32, u32, u32, u32)],
2686 msaa_samples: u32,
2687 base_config: &PlotRenderConfig,
2688 ) -> Result<(), Box<dyn std::error::Error>> {
2689 log::debug!(
2690 "runmat-plot: renderer.axes_to_viewports.start viewport_count={} msaa_samples={} width={} height={}",
2691 axes_viewports.len(),
2692 msaa_samples,
2693 base_config.width,
2694 base_config.height
2695 );
2696 let mut axes_to_nodes: std::collections::HashMap<usize, Vec<crate::core::scene::NodeId>> =
2698 std::collections::HashMap::new();
2699 for node in self.scene.get_visible_nodes() {
2700 axes_to_nodes
2701 .entry(node.axes_index)
2702 .or_default()
2703 .push(node.id);
2704 }
2705
2706 if self.axes_cameras.is_empty() {
2707 self.axes_cameras.push(Self::create_default_camera());
2708 }
2709 self.wgpu_renderer
2710 .ensure_axes_uniform_capacity(axes_viewports.len().max(1));
2711
2712 let all_ids: Vec<crate::core::scene::NodeId> = self
2714 .scene
2715 .get_visible_nodes()
2716 .into_iter()
2717 .map(|n| n.id)
2718 .collect();
2719 let active_axes: Vec<usize> = axes_viewports
2720 .iter()
2721 .enumerate()
2722 .filter_map(|(ax_idx, _)| {
2723 axes_to_nodes
2724 .get(&ax_idx)
2725 .filter(|ids| !ids.is_empty())
2726 .map(|_| ax_idx)
2727 })
2728 .collect();
2729 if active_axes.is_empty() {
2730 log::debug!("runmat-plot: renderer.axes_to_viewports.no_active_axes");
2731 return Ok(());
2732 }
2733
2734 self.wgpu_renderer.ensure_msaa(msaa_samples.max(1));
2735 let shared_msaa_view = if self.wgpu_renderer.msaa_sample_count > 1 {
2736 Some(self.wgpu_renderer.ensure_msaa_color_view())
2737 } else {
2738 None
2739 };
2740
2741 for (ax_idx, viewport) in axes_viewports.iter().enumerate() {
2742 log::debug!(
2743 "runmat-plot: renderer.axes_to_viewports.viewport axes_index={} viewport=({}, {}, {}, {})",
2744 ax_idx,
2745 viewport.0,
2746 viewport.1,
2747 viewport.2,
2748 viewport.3
2749 );
2750 let ids_for_axes = axes_to_nodes.get(&ax_idx).cloned().unwrap_or_default();
2751 if ids_for_axes.is_empty() {
2752 log::debug!(
2753 "runmat-plot: renderer.axes_to_viewports.skip_empty_axes axes_index={}",
2754 ax_idx
2755 );
2756 continue;
2757 }
2758
2759 let mut hidden_ids: Vec<crate::core::scene::NodeId> = Vec::new();
2761 for id in &all_ids {
2762 if !ids_for_axes.contains(id) {
2763 if let Some(node) = self.scene.get_node_mut(*id) {
2764 if node.visible {
2765 node.visible = false;
2766 hidden_ids.push(*id);
2767 }
2768 }
2769 }
2770 }
2771 let cam = self
2773 .axes_cameras
2774 .get(ax_idx)
2775 .cloned()
2776 .unwrap_or_else(Self::create_default_camera);
2777 let _ = self.calculate_data_bounds();
2778
2779 let mut cfg = base_config.clone();
2781 cfg.width = viewport.2;
2782 cfg.height = viewport.3;
2783 cfg.msaa_samples = msaa_samples.max(1);
2784 let is_first_axes = Some(&ax_idx) == active_axes.first();
2785 let is_last_axes = Some(&ax_idx) == active_axes.last();
2786 log::debug!(
2787 "runmat-plot: renderer.axes_to_viewports.axes_ready axes_index={} node_count={} first_axes={} last_axes={}",
2788 ax_idx,
2789 ids_for_axes.len(),
2790 is_first_axes,
2791 is_last_axes
2792 );
2793 let render_target = if let Some(ref msaa_view) = shared_msaa_view {
2794 RenderTarget {
2795 view: msaa_view.as_ref(),
2796 resolve_target: if is_last_axes {
2797 Some(target_view)
2798 } else {
2799 None
2800 },
2801 }
2802 } else {
2803 RenderTarget {
2804 view: target_view,
2805 resolve_target: None,
2806 }
2807 };
2808 let _ = self.render_camera_to_target_viewport(
2809 encoder,
2810 render_target,
2811 *viewport,
2812 &cfg,
2813 &cam,
2814 ax_idx,
2815 is_first_axes,
2816 )?;
2817 log::debug!(
2818 "runmat-plot: renderer.axes_to_viewports.axes_render_ok axes_index={}",
2819 ax_idx
2820 );
2821
2822 for id in hidden_ids {
2824 if let Some(node) = self.scene.get_node_mut(id) {
2825 node.visible = true;
2826 }
2827 }
2828 }
2829 log::debug!("runmat-plot: renderer.axes_to_viewports.ok");
2830 Ok(())
2831 }
2832
2833 fn create_default_camera() -> Camera {
2835 let mut camera = Camera::new();
2836 camera.projection = crate::core::camera::ProjectionType::Orthographic {
2837 left: -5.0,
2838 right: 5.0,
2839 bottom: -5.0,
2840 top: 5.0,
2841 near: -10.0,
2843 far: 10.0,
2844 };
2845 camera.depth_mode = DepthMode::default();
2846 camera.position = Vec3::new(0.0, 0.0, 1.0);
2848 camera.target = Vec3::new(0.0, 0.0, 0.0);
2849 camera.up = Vec3::new(0.0, 1.0, 0.0);
2850 camera
2851 }
2852
2853 pub fn camera(&self) -> &Camera {
2857 self.axes_cameras
2858 .first()
2859 .expect("axes_cameras must contain at least one camera")
2860 }
2861
2862 pub fn camera_mut(&mut self) -> &mut Camera {
2864 self.axes_cameras
2865 .first_mut()
2866 .expect("axes_cameras must contain at least one camera")
2867 }
2868
2869 pub fn axes_camera(&self, axes_index: usize) -> Option<&Camera> {
2870 self.axes_cameras.get(axes_index)
2871 }
2872
2873 pub fn scene(&self) -> &Scene {
2875 &self.scene
2876 }
2877
2878 pub fn scene_statistics(&self) -> crate::core::SceneStatistics {
2880 self.scene.statistics()
2881 }
2882
2883 pub fn view_bounds(&self) -> Option<(f64, f64, f64, f64)> {
2885 match self.camera().projection {
2886 crate::core::camera::ProjectionType::Orthographic {
2887 left,
2888 right,
2889 bottom,
2890 top,
2891 ..
2892 } => Some((left as f64, right as f64, bottom as f64, top as f64)),
2893 _ => None,
2894 }
2895 }
2896
2897 pub fn overlay_show_grid(&self) -> bool {
2899 self.figure_show_grid
2900 }
2901 pub fn overlay_show_minor_grid(&self) -> bool {
2902 self.figure_show_minor_grid
2903 }
2904 pub fn overlay_show_grid_for_axes(&self, axes_index: usize) -> bool {
2905 self.last_figure
2906 .as_ref()
2907 .and_then(|f| f.axes_metadata(axes_index))
2908 .map(|m| m.grid_enabled)
2909 .unwrap_or(self.figure_show_grid)
2910 }
2911 pub fn overlay_show_minor_grid_for_axes(&self, axes_index: usize) -> bool {
2912 Self::minor_grid_for_axes(
2913 self.last_figure.as_ref(),
2914 self.figure_show_minor_grid,
2915 axes_index,
2916 )
2917 }
2918
2919 fn minor_grid_for_axes(
2920 last_figure: Option<&crate::plots::Figure>,
2921 figure_show_minor_grid: bool,
2922 axes_index: usize,
2923 ) -> bool {
2924 last_figure
2925 .map(|f| f.minor_grid_enabled_for_axes(axes_index))
2926 .unwrap_or(figure_show_minor_grid)
2927 }
2928
2929 fn push_vertical_grid_lines(
2930 vertices: &mut Vec<Vertex>,
2931 x_bounds: (f64, f64),
2932 y_bounds: (f64, f64),
2933 step: f64,
2934 color: Vec4,
2935 skip_major: Option<(f64, f64)>,
2936 ) {
2937 Self::for_each_grid_position(x_bounds.0, x_bounds.1, step, skip_major, |x| {
2938 let x = x as f32;
2939 let y0 = y_bounds.0 as f32;
2940 let y1 = y_bounds.1 as f32;
2941 if x.is_finite() && y0.is_finite() && y1.is_finite() {
2942 vertices.push(Vertex::new(Vec3::new(x, y0, 0.0), color));
2943 vertices.push(Vertex::new(Vec3::new(x, y1, 0.0), color));
2944 }
2945 });
2946 }
2947
2948 fn push_horizontal_grid_lines(
2949 vertices: &mut Vec<Vertex>,
2950 y_bounds: (f64, f64),
2951 x_bounds: (f64, f64),
2952 step: f64,
2953 color: Vec4,
2954 skip_major: Option<(f64, f64)>,
2955 ) {
2956 Self::for_each_grid_position(y_bounds.0, y_bounds.1, step, skip_major, |y| {
2957 let y = y as f32;
2958 let x0 = x_bounds.0 as f32;
2959 let x1 = x_bounds.1 as f32;
2960 if y.is_finite() && x0.is_finite() && x1.is_finite() {
2961 vertices.push(Vertex::new(Vec3::new(x0, y, 0.0), color));
2962 vertices.push(Vertex::new(Vec3::new(x1, y, 0.0), color));
2963 }
2964 });
2965 }
2966
2967 fn for_each_grid_position(
2968 min: f64,
2969 max: f64,
2970 step: f64,
2971 skip_major: Option<(f64, f64)>,
2972 mut visit: impl FnMut(f64),
2973 ) {
2974 if !min.is_finite() || !max.is_finite() || !step.is_finite() || step <= 0.0 || min > max {
2975 return;
2976 }
2977 let start = (min / step).ceil() * step;
2978 if !start.is_finite() {
2979 return;
2980 }
2981
2982 let mut value = start;
2983 for _ in 0..MAX_2D_GRID_LINES_PER_AXIS {
2984 if value > max {
2985 break;
2986 }
2987 let is_major = skip_major
2988 .map(|(major_step, tolerance)| {
2989 major_step.is_finite()
2990 && major_step > 0.0
2991 && (value - (value / major_step).round() * major_step).abs() <= tolerance
2992 })
2993 .unwrap_or(false);
2994 if !is_major {
2995 visit(value);
2996 }
2997
2998 let next = value + step;
2999 if !next.is_finite() || next <= value {
3000 break;
3001 }
3002 value = next;
3003 }
3004 }
3005 pub fn overlay_show_box(&self) -> bool {
3006 self.figure_show_box
3007 }
3008 pub fn overlay_show_box_for_axes(&self, axes_index: usize) -> bool {
3009 self.last_figure
3010 .as_ref()
3011 .and_then(|f| f.axes_metadata(axes_index))
3012 .map(|m| m.box_enabled)
3013 .unwrap_or(self.figure_show_box)
3014 }
3015 pub fn overlay_title(&self) -> Option<&String> {
3016 self.figure_title.as_ref()
3017 }
3018 pub fn overlay_sg_title(&self) -> Option<&String> {
3019 self.figure_sg_title.as_ref()
3020 }
3021 pub fn overlay_sg_title_style(&self) -> &TextStyle {
3022 &self.figure_sg_title_style
3023 }
3024 pub fn overlay_title_for_axes(&self, axes_index: usize) -> Option<&String> {
3025 self.last_figure
3026 .as_ref()
3027 .and_then(|f| f.axes_metadata(axes_index))
3028 .and_then(|m| m.title.as_ref())
3029 }
3030 pub fn overlay_x_label(&self) -> Option<&String> {
3031 self.figure_x_label.as_ref()
3032 }
3033 pub fn overlay_x_label_for_axes(&self, axes_index: usize) -> Option<&String> {
3034 self.last_figure
3035 .as_ref()
3036 .and_then(|f| f.axes_metadata(axes_index))
3037 .and_then(|m| m.x_label.as_ref())
3038 }
3039 pub fn overlay_x_label_style_for_axes(&self, axes_index: usize) -> Option<&TextStyle> {
3040 self.last_figure
3041 .as_ref()
3042 .and_then(|f| f.axes_metadata(axes_index))
3043 .map(|m| &m.x_label_style)
3044 }
3045 pub fn overlay_axes_style_for_axes(&self, axes_index: usize) -> Option<&TextStyle> {
3046 self.last_figure
3047 .as_ref()
3048 .and_then(|f| f.axes_metadata(axes_index))
3049 .map(|m| &m.axes_style)
3050 }
3051 pub fn overlay_y_label(&self) -> Option<&String> {
3052 self.figure_y_label.as_ref()
3053 }
3054 pub fn overlay_y_label_for_axes(&self, axes_index: usize) -> Option<&String> {
3055 self.last_figure
3056 .as_ref()
3057 .and_then(|f| f.axes_metadata(axes_index))
3058 .and_then(|m| m.y_label.as_ref())
3059 }
3060 pub fn overlay_y_label_style_for_axes(&self, axes_index: usize) -> Option<&TextStyle> {
3061 self.last_figure
3062 .as_ref()
3063 .and_then(|f| f.axes_metadata(axes_index))
3064 .map(|m| &m.y_label_style)
3065 }
3066 pub fn overlay_z_label(&self) -> Option<&String> {
3067 self.figure_z_label.as_ref()
3068 }
3069 pub fn overlay_z_label_for_axes(&self, axes_index: usize) -> Option<&String> {
3070 self.last_figure
3071 .as_ref()
3072 .and_then(|f| f.axes_metadata(axes_index))
3073 .and_then(|m| m.z_label.as_ref())
3074 }
3075 pub fn overlay_z_label_style_for_axes(&self, axes_index: usize) -> Option<&TextStyle> {
3076 self.last_figure
3077 .as_ref()
3078 .and_then(|f| f.axes_metadata(axes_index))
3079 .map(|m| &m.z_label_style)
3080 }
3081 pub fn active_axes_pie_labels(&self) -> Vec<(String, glam::Vec2)> {
3082 let Some(fig) = self.last_figure.as_ref() else {
3083 return Vec::new();
3084 };
3085 fig.pie_labels_for_axes(fig.active_axes_index)
3086 .into_iter()
3087 .map(|entry| (entry.label, entry.position))
3088 .collect()
3089 }
3090 pub fn pie_labels_for_axes(&self, axes_index: usize) -> Vec<(String, glam::Vec2)> {
3091 let Some(fig) = self.last_figure.as_ref() else {
3092 return Vec::new();
3093 };
3094 fig.pie_labels_for_axes(axes_index)
3095 .into_iter()
3096 .map(|entry| (entry.label, entry.position))
3097 .collect()
3098 }
3099
3100 pub fn world_text_annotations_for_axes(
3101 &self,
3102 axes_index: usize,
3103 ) -> Vec<(glam::Vec3, String, TextStyle)> {
3104 self.last_figure
3105 .as_ref()
3106 .map(|f| {
3107 f.axes_text_annotations(axes_index)
3108 .iter()
3109 .map(|annotation| {
3110 (
3111 annotation.position,
3112 annotation.text.clone(),
3113 annotation.style.clone(),
3114 )
3115 })
3116 .collect()
3117 })
3118 .unwrap_or_default()
3119 }
3120
3121 pub fn world_axis_label_annotations_for_axes(
3122 &self,
3123 axes_index: usize,
3124 ) -> Vec<(glam::Vec3, String, TextStyle)> {
3125 let Some(fig) = self.last_figure.as_ref() else {
3126 return Vec::new();
3127 };
3128 let Some(meta) = fig.axes_metadata(axes_index) else {
3129 return Vec::new();
3130 };
3131 let Some(bounds) = self.display_bounds_3d_for_axes(axes_index) else {
3132 return Vec::new();
3133 };
3134 let dx = (bounds.max.x - bounds.min.x).abs().max(1.0e-3);
3135 let dy = (bounds.max.y - bounds.min.y).abs().max(1.0e-3);
3136 let dz = (bounds.max.z - bounds.min.z).abs().max(1.0e-3);
3137 let camera = self
3138 .axes_camera(axes_index)
3139 .or_else(|| Some(self.camera()))
3140 .expect("plot renderer must always have a camera");
3141 let center = (bounds.min + bounds.max) * 0.5;
3142 let cam_delta = camera.position - center;
3143 let sx = if cam_delta.x >= 0.0 { 1.0 } else { -1.0 };
3144 let sy = if cam_delta.y >= 0.0 { 1.0 } else { -1.0 };
3145 let sz = if cam_delta.z >= 0.0 { 1.0 } else { -1.0 };
3146 let x_anchor = glam::Vec3::new(bounds.min.x + dx * 0.82, bounds.min.y, bounds.min.z)
3147 + glam::Vec3::new(0.0, -sy * dy * 0.10, -sz * dz * 0.08);
3148 let y_anchor = glam::Vec3::new(bounds.min.x, bounds.min.y + dy * 0.82, bounds.min.z)
3149 + glam::Vec3::new(-sx * dx * 0.10, 0.0, -sz * dz * 0.08);
3150 let z_anchor = glam::Vec3::new(bounds.min.x, bounds.min.y, bounds.min.z + dz * 0.82)
3151 + glam::Vec3::new(-sx * dx * 0.08, -sy * dy * 0.08, 0.0);
3152 let mut out = Vec::new();
3153 if let Some(label) = meta.x_label.clone().filter(|s| !s.is_empty()) {
3154 out.push((x_anchor, label, meta.x_label_style.clone()));
3155 }
3156 if let Some(label) = meta.y_label.clone().filter(|s| !s.is_empty()) {
3157 out.push((y_anchor, label, meta.y_label_style.clone()));
3158 }
3159 if let Some(label) = meta.z_label.clone().filter(|s| !s.is_empty()) {
3160 out.push((z_anchor, label, meta.z_label_style.clone()));
3161 }
3162 out
3163 }
3164 pub fn overlay_show_legend(&self) -> bool {
3165 self.figure_show_legend
3166 }
3167 pub fn overlay_show_legend_for_axes(&self, axes_index: usize) -> bool {
3168 self.last_figure
3169 .as_ref()
3170 .and_then(|f| f.axes_metadata(axes_index))
3171 .map(|m| m.legend_enabled)
3172 .unwrap_or(self.figure_show_legend)
3173 }
3174 pub fn overlay_legend_entries(&self) -> &Vec<LegendEntry> {
3175 &self.legend_entries
3176 }
3177 pub fn overlay_legend_entries_for_axes(&self, axes_index: usize) -> Vec<LegendEntry> {
3178 self.last_figure
3179 .as_ref()
3180 .map(|f| f.legend_entries_for_axes(axes_index))
3181 .unwrap_or_default()
3182 }
3183 pub fn overlay_x_log(&self) -> bool {
3184 self.figure_x_log
3185 }
3186 pub fn overlay_x_log_for_axes(&self, axes_index: usize) -> bool {
3187 self.last_figure
3188 .as_ref()
3189 .and_then(|f| f.axes_metadata(axes_index))
3190 .map(|m| m.x_log)
3191 .unwrap_or(self.figure_x_log)
3192 }
3193 pub fn overlay_y_log(&self) -> bool {
3194 self.figure_y_log
3195 }
3196 pub fn overlay_y_log_for_axes(&self, axes_index: usize) -> bool {
3197 self.last_figure
3198 .as_ref()
3199 .and_then(|f| f.axes_metadata(axes_index))
3200 .map(|m| m.y_log)
3201 .unwrap_or(self.figure_y_log)
3202 }
3203 pub fn overlay_colormap(&self) -> ColorMap {
3204 self.figure_colormap
3205 }
3206 pub fn overlay_colorbar_enabled(&self) -> bool {
3207 self.figure_colorbar_enabled
3208 }
3209 pub fn figure_axes_grid(&self) -> (usize, usize) {
3211 self.last_figure
3212 .as_ref()
3213 .map(|f| f.axes_grid())
3214 .unwrap_or((1, 1))
3215 }
3216 pub fn overlay_categorical_labels(&self) -> Option<(bool, &Vec<String>)> {
3218 if let (Some(is_x), Some(labels)) = (
3219 &self.figure_categorical_is_x,
3220 &self.figure_categorical_labels,
3221 ) {
3222 Some((*is_x, labels))
3223 } else {
3224 None
3225 }
3226 }
3227
3228 pub fn overlay_categorical_labels_for_axes(
3229 &self,
3230 axes_index: usize,
3231 ) -> Option<(bool, Vec<String>)> {
3232 self.last_figure
3233 .as_ref()
3234 .and_then(|f| f.categorical_axis_labels_for_axes(axes_index))
3235 }
3236
3237 pub fn overlay_x_tick_labels_for_axes(&self, axes_index: usize) -> Option<Vec<String>> {
3238 self.last_figure
3239 .as_ref()
3240 .and_then(|f| f.x_axis_tick_labels_for_axes(axes_index))
3241 }
3242
3243 pub fn overlay_y_tick_labels_for_axes(&self, axes_index: usize) -> Option<Vec<String>> {
3244 self.last_figure
3245 .as_ref()
3246 .and_then(|f| f.y_axis_tick_labels_for_axes(axes_index))
3247 }
3248
3249 pub fn overlay_histogram_edges_for_axes(&self, axes_index: usize) -> Option<(bool, Vec<f64>)> {
3250 self.last_figure
3251 .as_ref()
3252 .and_then(|f| f.histogram_axis_edges_for_axes(axes_index))
3253 }
3254
3255 pub fn overlay_display_bounds_for_axes(
3257 &self,
3258 axes_index: usize,
3259 ) -> Option<(f64, f64, f64, f64)> {
3260 self.display_bounds_for_axes(axes_index)
3261 }
3262
3263 pub fn data_bounds(&self) -> Option<(f64, f64, f64, f64)> {
3265 let base = self.data_bounds;
3266 base.map(|(bx_min, bx_max, by_min, by_max)| {
3267 let (mut x_min, mut x_max) = (bx_min, bx_max);
3268 let (mut y_min, mut y_max) = (by_min, by_max);
3269 if let Some((xl, xr)) = self.figure_x_limits {
3270 x_min = xl;
3271 x_max = xr;
3272 }
3273 if let Some((yl, yr)) = self.figure_y_limits {
3274 y_min = yl;
3275 y_max = yr;
3276 }
3277 (x_min, x_max, y_min, y_max)
3278 })
3279 }
3280
3281 pub fn axes_camera_mut(&mut self, idx: usize) -> Option<&mut Camera> {
3283 self.axes_cameras.get_mut(idx)
3284 }
3285
3286 pub fn view_bounds_for_axes(&self, idx: usize) -> Option<(f64, f64, f64, f64)> {
3288 if let Some(cam) = self.axes_cameras.get(idx) {
3289 if let crate::core::camera::ProjectionType::Orthographic {
3290 left,
3291 right,
3292 bottom,
3293 top,
3294 ..
3295 } = cam.projection
3296 {
3297 return Some((left as f64, right as f64, bottom as f64, top as f64));
3298 }
3299 }
3300 None
3301 }
3302
3303 pub fn axes_bounds(&self, axes_index: usize) -> Option<crate::core::BoundingBox> {
3304 let mut min = Vec3::new(f32::INFINITY, f32::INFINITY, f32::INFINITY);
3305 let mut max = Vec3::new(f32::NEG_INFINITY, f32::NEG_INFINITY, f32::NEG_INFINITY);
3306 let mut saw_any = false;
3307
3308 for node in self.scene.get_visible_nodes() {
3309 if node.axes_index != axes_index {
3310 continue;
3311 }
3312 let Some(render_data) = &node.render_data else {
3313 continue;
3314 };
3315 if let Some(bounds) = render_data.bounds {
3316 min = min.min(bounds.min);
3317 max = max.max(bounds.max);
3318 saw_any = true;
3319 continue;
3320 }
3321 for v in &render_data.vertices {
3322 let p = Vec3::new(v.position[0], v.position[1], v.position[2]);
3323 min = min.min(p);
3324 max = max.max(p);
3325 saw_any = true;
3326 }
3327 }
3328
3329 if !saw_any {
3330 return None;
3331 }
3332 Some(crate::core::BoundingBox { min, max })
3333 }
3334
3335 pub fn export_figure_clone(&self) -> crate::plots::Figure {
3337 if let Some(f) = &self.last_figure {
3338 return f.clone();
3339 }
3340 let mut fig = crate::plots::Figure::new();
3342 fig.title = self.figure_title.clone();
3343 fig.sg_title = self.figure_sg_title.clone();
3344 fig.sg_title_style = self.figure_sg_title_style.clone();
3345 fig.x_label = self.figure_x_label.clone();
3346 fig.y_label = self.figure_y_label.clone();
3347 fig.legend_enabled = self.figure_show_legend;
3348 fig.grid_enabled = self.figure_show_grid;
3349 fig.minor_grid_enabled = self.figure_show_minor_grid;
3350 fig.box_enabled = self.figure_show_box;
3351 fig.x_limits = self.figure_x_limits;
3352 fig.y_limits = self.figure_y_limits;
3353 fig.x_log = self.figure_x_log;
3354 fig.y_log = self.figure_y_log;
3355 fig.axis_equal = self.figure_axis_equal;
3356 fig.colormap = self.figure_colormap;
3357 fig.colorbar_enabled = self.figure_colorbar_enabled;
3358 let (rows, cols) = self.figure_axes_grid();
3359 fig.set_subplot_grid(rows, cols);
3360 fig
3361 }
3362}
3363
3364#[cfg(test)]
3365mod tests {
3366 use super::*;
3367 use crate::plots::{figure::PlotElement, Figure, PatchPlot};
3368
3369 fn patch_element(vertices: Vec<Vec3>) -> PlotElement {
3370 PlotElement::Patch(PatchPlot::new(vertices, vec![vec![0, 1, 2]]).unwrap())
3371 }
3372
3373 #[test]
3374 fn patch_3d_detection_preserves_small_scene_depth() {
3375 let patch = patch_element(vec![
3376 Vec3::new(0.0, 0.0, 0.0),
3377 Vec3::new(1.0e-4, 0.0, 0.0),
3378 Vec3::new(0.0, 1.0e-4, 1.0e-8),
3379 ]);
3380
3381 assert!(PlotRenderer::plot_element_is_3d(&patch));
3382 }
3383
3384 #[test]
3385 fn patch_3d_detection_ignores_large_scene_relative_noise() {
3386 let patch = patch_element(vec![
3387 Vec3::new(0.0, 0.0, 0.0),
3388 Vec3::new(1.0e6, 0.0, 0.0),
3389 Vec3::new(0.0, 1.0e6, 0.5),
3390 ]);
3391
3392 assert!(!PlotRenderer::plot_element_is_3d(&patch));
3393 }
3394
3395 #[test]
3396 fn applies_z_limits_to_3d_display_bounds() {
3397 let mut figure = Figure::new();
3398 figure.set_axes_z_limits(0, Some((-2.0, 3.0)));
3399 let bounds = BoundingBox::new(Vec3::new(-1.0, -1.0, -10.0), Vec3::new(1.0, 1.0, 10.0));
3400
3401 let limited = PlotRenderer::apply_3d_display_limits_to_bounds(bounds, Some(&figure), 0);
3402
3403 assert_eq!(limited.min.z, -2.0);
3404 assert_eq!(limited.max.z, 3.0);
3405 assert_eq!(limited.min.x, -1.0);
3406 assert_eq!(limited.max.x, 1.0);
3407 }
3408
3409 #[test]
3410 fn degenerate_z_bounds_remain_finite_for_3d_display_bounds() {
3411 let figure = Figure::new();
3412 let bounds = BoundingBox::new(Vec3::new(-2.0, -2.0, 0.0), Vec3::new(2.0, 2.0, 0.0));
3413
3414 let display = PlotRenderer::apply_3d_display_limits_to_bounds(bounds, Some(&figure), 0);
3415
3416 assert!(PlotRenderer::bounds_are_finite(display));
3417 assert_eq!(display.min.z, 0.0);
3418 assert_eq!(display.max.z, 0.0);
3419 }
3420
3421 #[test]
3422 fn axes_view_contract_tracks_subplot_limits() {
3423 let mut base = Figure::new();
3424 base.set_subplot_grid(2, 1);
3425 let mut limited = base.clone();
3426 limited.set_axes_limits(0, Some((0.0, 30.0)), None);
3427 limited.set_axes_limits(1, Some((200.0, 450.0)), None);
3428
3429 let base_contract = PlotRenderer::axes_view_contract_for_figure(&base);
3430 let limited_contract = PlotRenderer::axes_view_contract_for_figure(&limited);
3431
3432 assert_ne!(base_contract, limited_contract);
3433 assert_eq!(limited_contract.axes[0].x_limits, Some((0.0, 30.0)));
3434 assert_eq!(limited_contract.axes[1].x_limits, Some((200.0, 450.0)));
3435 }
3436
3437 #[test]
3438 fn figure_level_minor_grid_survives_default_axes_metadata() {
3439 let mut figure = Figure::new();
3440 figure.minor_grid_enabled = true;
3441
3442 assert!(!figure.axes_metadata(0).unwrap().minor_grid_enabled);
3443 assert!(PlotRenderer::minor_grid_for_axes(
3444 Some(&figure),
3445 figure.minor_grid_enabled,
3446 0
3447 ));
3448 }
3449
3450 #[test]
3451 fn explicit_axes_minor_grid_false_overrides_figure_level_minor_grid() {
3452 let mut figure = Figure::new();
3453 figure.minor_grid_enabled = true;
3454 figure.set_axes_minor_grid_enabled(0, false);
3455
3456 let meta = figure.axes_metadata(0).unwrap();
3457 assert!(!meta.minor_grid_enabled);
3458 assert!(meta.minor_grid_explicit);
3459 assert!(!PlotRenderer::minor_grid_for_axes(
3460 Some(&figure),
3461 figure.minor_grid_enabled,
3462 0
3463 ));
3464 }
3465}
3466
3467pub mod plot_utils {
3469 pub fn generate_major_ticks(min: f64, max: f64) -> Vec<f64> {
3470 if !(min.is_finite() && max.is_finite()) || max <= min {
3471 return Vec::new();
3472 }
3473 let range = (max - min).max(1e-9);
3474 let step = calculate_tick_interval(range);
3475 if !(step.is_finite() && step > 0.0) {
3476 return Vec::new();
3477 }
3478
3479 let mut ticks = Vec::new();
3480 let mut value = (min / step).ceil() * step;
3481 let epsilon = range * 1e-6 + step * 1e-6;
3482 while value <= max + epsilon {
3483 let snapped = if value.abs() < epsilon { 0.0 } else { value };
3484 ticks.push(snapped);
3485 value += step;
3486 if ticks.len() > 64 {
3487 break;
3488 }
3489 }
3490
3491 let endpoint_tol = step * 0.18;
3492 let near_min = ticks.iter().any(|t| (*t - min).abs() <= endpoint_tol);
3493 let near_max = ticks.iter().any(|t| (*t - max).abs() <= endpoint_tol);
3494 if !near_min {
3495 ticks.insert(0, min);
3496 }
3497 if !near_max {
3498 ticks.push(max);
3499 }
3500
3501 ticks.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
3502 ticks.dedup_by(|a, b| (*a - *b).abs() <= endpoint_tol * 0.5);
3503 ticks
3504 }
3505
3506 pub fn calculate_tick_interval(range: f64) -> f64 {
3508 let magnitude = 10.0_f64.powf(range.log10().floor());
3509 let normalized = range / magnitude;
3510
3511 let nice_interval = if normalized <= 1.0 {
3512 0.2
3513 } else if normalized <= 2.0 {
3514 0.5
3515 } else if normalized <= 5.0 {
3516 1.0
3517 } else {
3518 2.0
3519 };
3520
3521 nice_interval * magnitude
3522 }
3523
3524 pub fn format_tick_label(value: f64) -> String {
3526 fn trim_fixed(mut s: String) -> String {
3527 if s.contains('.') {
3528 while s.ends_with('0') {
3529 s.pop();
3530 }
3531 if s.ends_with('.') {
3532 s.pop();
3533 }
3534 }
3535 if s == "-0" {
3536 "0".to_string()
3537 } else {
3538 s
3539 }
3540 }
3541
3542 if value.abs() < 0.001 {
3543 "0".to_string()
3544 } else if value.abs() >= 1000.0 || value.fract().abs() < 0.0005 {
3545 format!("{value:.0}")
3546 } else if value.abs() < 0.1 {
3547 trim_fixed(format!("{value:.3}"))
3548 } else if value.abs() < 10.0 {
3549 trim_fixed(format!("{value:.2}"))
3550 } else {
3551 trim_fixed(format!("{value:.1}"))
3552 }
3553 }
3554
3555 pub fn generate_grid_lines(
3557 bounds: (f64, f64, f64, f64),
3558 plot_rect: (f32, f32, f32, f32), ) -> Vec<(f32, f32, f32, f32)> {
3560 let (x_min, x_max, y_min, y_max) = bounds;
3562 let (left, right, bottom, top) = plot_rect;
3563
3564 let mut lines = Vec::new();
3565
3566 let x_range = x_max - x_min;
3568 let x_interval = calculate_tick_interval(x_range);
3569 let mut x_val = (x_min / x_interval).ceil() * x_interval;
3570
3571 while x_val <= x_max {
3572 let x_screen = left + ((x_val - x_min) / x_range) as f32 * (right - left);
3573 lines.push((x_screen, bottom, x_screen, top));
3574 x_val += x_interval;
3575 }
3576
3577 let y_range = y_max - y_min;
3579 let y_interval = calculate_tick_interval(y_range);
3580 let mut y_val = (y_min / y_interval).ceil() * y_interval;
3581
3582 while y_val <= y_max {
3583 let y_screen = bottom + ((y_val - y_min) / y_range) as f32 * (top - bottom);
3584 lines.push((left, y_screen, right, y_screen));
3585 y_val += y_interval;
3586 }
3587
3588 lines
3589 }
3590}