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