1use crate::core::renderer::{DirectUniforms, Vertex};
8use crate::core::{
9 BoundingBox, Camera, CameraViewPreset, ClipPolicy, DepthMode, Scene, WgpuRenderer,
10};
11use crate::geometry_scene::{
12 GeometryScene, GeometrySceneCacheKey, GeometrySceneOverlay, GeometryScenePresentation,
13 GeometrySceneViewPreset,
14};
15use crate::plots::figure::{LegendEntry, TextStyle};
16use crate::plots::surface::ColorMap;
17use crate::plots::{AxesKind, Figure};
18use glam::{Mat4, Vec3, Vec4};
19use runmat_time::Instant;
20use std::cell::RefCell;
21use std::collections::{BTreeSet, HashMap};
22use std::sync::Arc;
23#[cfg(not(target_arch = "wasm32"))]
24use std::sync::OnceLock;
25
26type ViewBounds2D = (f64, f64, f64, f64);
27type PerAxesViewBounds = Vec<Option<ViewBounds2D>>;
28
29#[derive(Clone, Debug)]
30struct CachedSceneBuffers {
31 vertex_signature: (usize, usize),
32 vertex_buffer: Arc<wgpu::Buffer>,
33 index_signature: Option<(usize, usize)>,
34 index_buffer: Option<Arc<wgpu::Buffer>>,
35}
36
37#[derive(Clone, Debug, PartialEq)]
38struct AxesViewContract {
39 rows: usize,
40 cols: usize,
41 axes: Vec<AxesViewContractEntry>,
42}
43
44#[derive(Clone, Debug, PartialEq)]
45struct AxesViewContractEntry {
46 has_3d_content: bool,
47 axes_kind: AxesKind,
48 x_limits: Option<(f64, f64)>,
49 y_limits: Option<(f64, f64)>,
50 z_limits: Option<(f64, f64)>,
51 axis_equal: bool,
52 x_log: bool,
53 y_log: bool,
54 view_azimuth_deg: Option<f32>,
55 view_elevation_deg: Option<f32>,
56 view_revision: u64,
57}
58
59const PATCH_3D_ABS_EPSILON: f32 = 1e-9;
60const PATCH_3D_REL_EPSILON: f32 = 1e-6;
61const MAX_2D_GRID_LINES_PER_AXIS: usize = 4096;
62
63fn render_item_diagnostics_enabled() -> bool {
64 #[cfg(target_arch = "wasm32")]
65 {
66 false
67 }
68
69 #[cfg(not(target_arch = "wasm32"))]
70 {
71 static ENABLED: OnceLock<bool> = OnceLock::new();
72 *ENABLED.get_or_init(|| match std::env::var("RUNMAT_PLOT_RENDER_ITEM_LOGS") {
73 Ok(value) => matches!(
74 value.trim().to_ascii_lowercase().as_str(),
75 "1" | "true" | "yes" | "on"
76 ),
77 Err(_) => false,
78 })
79 }
80}
81
82pub struct PlotRenderer {
84 pub wgpu_renderer: WgpuRenderer,
86
87 pub scene: Scene,
89
90 pub theme: crate::styling::PlotThemeConfig,
92
93 data_bounds: Option<(f64, f64, f64, f64)>,
95 needs_update: bool,
96
97 figure_title: Option<String>,
99 figure_sg_title: Option<String>,
100 figure_sg_title_style: TextStyle,
101 figure_x_label: Option<String>,
102 figure_y_label: Option<String>,
103 figure_z_label: Option<String>,
104 figure_show_grid: bool,
105 figure_show_minor_grid: bool,
106 figure_show_legend: bool,
107 figure_show_box: bool,
108 figure_x_limits: Option<(f64, f64)>,
109 figure_y_limits: Option<(f64, f64)>,
110 legend_entries: Vec<LegendEntry>,
111 figure_x_log: bool,
112 figure_y_log: bool,
113 figure_axis_equal: bool,
114 figure_colormap: ColorMap,
115 figure_colorbar_enabled: bool,
116 figure_categorical_is_x: Option<bool>,
118 figure_categorical_labels: Option<Vec<String>>,
119 axes_cameras: Vec<Camera>,
121 pub(crate) last_figure: Option<crate::plots::Figure>,
123 last_geometry_scene_key: Option<GeometrySceneCacheKey>,
125 last_geometry_scene: Option<GeometryScene>,
126 geometry_overlay: Option<GeometrySceneOverlay>,
127 geometry_presentation: GeometryScenePresentation,
128 geometry_xray_enabled: bool,
129 geometry_node_owner_ids: HashMap<u64, Vec<String>>,
130 geometry_hidden_owner_node_ids: BTreeSet<String>,
131
132 last_scene_viewport_px: Option<(u32, u32)>,
135 last_axes_plot_sizes_px: Option<Vec<(u32, u32)>>,
137 last_axes_view_bounds: Option<PerAxesViewBounds>,
139 last_axes_view_contract: Option<AxesViewContract>,
141
142 camera_auto_fit: bool,
144 axes_2d_camera_user_controlled: Vec<bool>,
147 axes_applied_view_revisions: Vec<Option<u64>>,
149 scene_buffer_cache: RefCell<HashMap<u64, CachedSceneBuffers>>,
151}
152
153#[derive(Debug, Clone)]
155pub struct PlotRenderConfig {
156 pub width: u32,
158 pub height: u32,
159
160 pub background_color: Vec4,
162
163 pub show_grid: bool,
165
166 pub show_axes: bool,
168
169 pub show_title: bool,
171
172 pub msaa_samples: u32,
174
175 pub depth_mode: DepthMode,
177
178 pub clip_policy: ClipPolicy,
180
181 pub theme: crate::styling::PlotThemeConfig,
183}
184
185impl Default for PlotRenderConfig {
186 fn default() -> Self {
187 Self {
188 width: 800,
189 height: 600,
190 background_color: Vec4::new(0.08, 0.09, 0.11, 1.0), show_grid: true,
192 show_axes: true,
193 show_title: true,
194 msaa_samples: 4,
195 depth_mode: DepthMode::default(),
196 clip_policy: ClipPolicy::default(),
197 theme: crate::styling::PlotThemeConfig::default(),
198 }
199 }
200}
201
202pub struct RenderTarget<'a> {
204 pub view: &'a wgpu::TextureView,
205 pub resolve_target: Option<&'a wgpu::TextureView>,
206}
207
208#[derive(Debug)]
210pub struct RenderResult {
211 pub success: bool,
213
214 pub data_bounds: Option<(f64, f64, f64, f64)>,
216
217 pub axes_viewports_px: Vec<(u32, u32, u32, u32)>,
222
223 pub vertex_count: usize,
225 pub triangle_count: usize,
226 pub render_time_ms: f64,
227}
228
229impl PlotRenderer {
230 pub fn on_surface_config_updated(&mut self) {
235 let current = (
236 self.wgpu_renderer.surface_config.width.max(1),
237 self.wgpu_renderer.surface_config.height.max(1),
238 );
239 if self.last_scene_viewport_px == Some(current) {
240 return;
241 }
242 let Some(figure) = self.last_figure.clone() else {
243 self.last_scene_viewport_px = Some(current);
244 return;
245 };
246 self.set_figure(figure);
248 }
249
250 fn prepare_buffers_for_render_data(
251 &self,
252 node_id: u64,
253 render_data: &crate::core::RenderData,
254 ) -> Option<(Arc<wgpu::Buffer>, Option<Arc<wgpu::Buffer>>)> {
255 let mut cache = self.scene_buffer_cache.borrow_mut();
256 let vertex_signature = (
257 render_data.vertices.as_ptr() as usize,
258 render_data.vertices.len(),
259 );
260 let index_signature = render_data
261 .indices
262 .as_ref()
263 .map(|indices| (indices.as_ptr() as usize, indices.len()));
264
265 if let Some(cached) = cache.get(&node_id) {
266 if cached.vertex_signature == vertex_signature
267 && cached.index_signature == index_signature
268 {
269 return Some((cached.vertex_buffer.clone(), cached.index_buffer.clone()));
270 }
271 }
272
273 let vertex_buffer = self
274 .wgpu_renderer
275 .vertex_buffer_from_sources(render_data.gpu_vertices.as_ref(), &render_data.vertices)?;
276 let index_buffer = render_data
277 .indices
278 .as_ref()
279 .map(|indices| Arc::new(self.wgpu_renderer.create_index_buffer(indices)));
280
281 cache.insert(
282 node_id,
283 CachedSceneBuffers {
284 vertex_signature,
285 vertex_buffer: vertex_buffer.clone(),
286 index_signature,
287 index_buffer: index_buffer.clone(),
288 },
289 );
290
291 Some((vertex_buffer, index_buffer))
292 }
293
294 fn gpu_indirect_args(render_data: &crate::core::RenderData) -> Option<(&wgpu::Buffer, u64)> {
295 render_data
296 .gpu_vertices
297 .as_ref()
298 .and_then(|buf| buf.indirect.as_ref())
299 .map(|indirect| (indirect.args.as_ref(), indirect.offset))
300 }
301
302 pub async fn new(
304 device: Arc<wgpu::Device>,
305 queue: Arc<wgpu::Queue>,
306 surface_config: wgpu::SurfaceConfiguration,
307 ) -> Result<Self, Box<dyn std::error::Error>> {
308 let wgpu_renderer = WgpuRenderer::new(device, queue, surface_config).await;
309 let scene = Scene::new();
310 let theme = crate::styling::PlotThemeConfig::default();
311
312 Ok(Self {
313 wgpu_renderer,
314 scene,
315 theme,
316 data_bounds: None,
317 needs_update: true,
318 figure_title: None,
319 figure_sg_title: None,
320 figure_sg_title_style: TextStyle::default(),
321 figure_x_label: None,
322 figure_y_label: None,
323 figure_z_label: None,
324 figure_show_grid: true,
325 figure_show_minor_grid: false,
326 figure_show_legend: true,
327 figure_show_box: true,
328 figure_x_limits: None,
329 figure_y_limits: None,
330 legend_entries: Vec::new(),
331 figure_x_log: false,
332 figure_y_log: false,
333 figure_axis_equal: false,
334 figure_colormap: ColorMap::Parula,
335 figure_colorbar_enabled: false,
336 figure_categorical_is_x: None,
337 figure_categorical_labels: None,
338 axes_cameras: vec![Self::create_default_camera()],
339 last_figure: None,
340 last_geometry_scene_key: None,
341 last_geometry_scene: None,
342 geometry_overlay: None,
343 geometry_presentation: GeometryScenePresentation::default(),
344 geometry_xray_enabled: false,
345 geometry_node_owner_ids: HashMap::new(),
346 geometry_hidden_owner_node_ids: BTreeSet::new(),
347 last_scene_viewport_px: None,
348 last_axes_plot_sizes_px: None,
349 last_axes_view_bounds: None,
350 last_axes_view_contract: None,
351 camera_auto_fit: true,
352 axes_2d_camera_user_controlled: vec![false],
353 axes_applied_view_revisions: vec![None],
354 scene_buffer_cache: RefCell::new(HashMap::new()),
355 })
356 }
357
358 fn plot_element_is_3d(plot: &crate::plots::figure::PlotElement) -> bool {
359 match plot {
360 crate::plots::figure::PlotElement::Surface(surface) => !surface.image_mode,
361 crate::plots::figure::PlotElement::Mesh(_) => true,
362 crate::plots::figure::PlotElement::Patch(patch) => {
363 if patch.force_3d() {
364 return true;
365 }
366
367 let mut max_xy = 0.0_f32;
368 let mut max_z = 0.0_f32;
369 for point in patch.vertices() {
370 max_xy = max_xy.max(point.x.abs().max(point.y.abs()));
371 max_z = max_z.max(point.z.abs());
372 }
373
374 max_z > PATCH_3D_ABS_EPSILON.max(max_xy * PATCH_3D_REL_EPSILON)
375 }
376 crate::plots::figure::PlotElement::Line3(_) => true,
377 crate::plots::figure::PlotElement::Scatter3(_) => true,
378 crate::plots::figure::PlotElement::Contour(contour) => contour.is_3d(),
379 _ => false,
380 }
381 }
382
383 pub fn axes_has_3d_content(&self, axes_index: usize) -> bool {
384 self.last_figure
385 .as_ref()
386 .map(|figure| {
387 figure
388 .plots()
389 .zip(figure.plot_axes_indices().iter().copied())
390 .any(|(plot, plot_axes_index)| {
391 plot_axes_index == axes_index && Self::plot_element_is_3d(plot)
392 })
393 })
394 .unwrap_or(false)
395 }
396
397 fn axes_view_contract_for_figure(figure: &Figure) -> AxesViewContract {
398 let (rows, cols) = figure.axes_grid();
399 let axes_count = rows.max(1) * cols.max(1);
400 let mut has_3d_content = vec![false; axes_count];
401 for (plot, axes_index) in figure
402 .plots()
403 .zip(figure.plot_axes_indices().iter().copied())
404 {
405 if axes_index < axes_count && Self::plot_element_is_3d(plot) {
406 has_3d_content[axes_index] = true;
407 }
408 }
409 let axes = (0..axes_count)
410 .map(|axes_index| {
411 let meta = figure.axes_metadata(axes_index);
412 AxesViewContractEntry {
413 has_3d_content: has_3d_content[axes_index],
414 axes_kind: meta.map(|m| m.axes_kind).unwrap_or(AxesKind::Cartesian),
415 x_limits: meta.and_then(|m| m.x_limits),
416 y_limits: meta.and_then(|m| m.y_limits),
417 z_limits: meta.and_then(|m| m.z_limits),
418 axis_equal: meta.map(|m| m.axis_equal).unwrap_or(false),
419 x_log: meta.map(|m| m.x_log).unwrap_or(false),
420 y_log: meta.map(|m| m.y_log).unwrap_or(false),
421 view_azimuth_deg: meta.and_then(|m| m.view_azimuth_deg),
422 view_elevation_deg: meta.and_then(|m| m.view_elevation_deg),
423 view_revision: meta.map(|m| m.view_revision).unwrap_or(0),
424 }
425 })
426 .collect();
427 AxesViewContract { rows, cols, axes }
428 }
429
430 pub fn note_camera_interaction(&mut self) {
432 if self.camera_auto_fit {
433 log::debug!(target: "runmat_plot", "camera_auto_fit disabled (user interaction)");
434 }
435 self.camera_auto_fit = false;
436 }
437
438 pub fn note_axes_camera_interaction(&mut self, axes_index: usize) {
439 self.note_camera_interaction();
440 if self.axes_has_3d_content(axes_index) {
441 return;
442 }
443 if let Some(flag) = self.axes_2d_camera_user_controlled.get_mut(axes_index) {
444 *flag = true;
445 }
446 }
447
448 pub fn set_axes_camera_interaction_flags(&mut self, flags: &[bool]) {
449 self.axes_2d_camera_user_controlled
450 .resize(self.axes_cameras.len(), false);
451 let mut any_user_controlled = false;
452 for idx in 0..self.axes_cameras.len() {
453 let controlled = flags.get(idx).copied().unwrap_or(false);
454 if let Some(flag) = self.axes_2d_camera_user_controlled.get_mut(idx) {
455 *flag = controlled;
456 }
457 any_user_controlled |= controlled;
458 }
459 if any_user_controlled {
460 self.note_camera_interaction();
461 }
462 }
463
464 pub fn axes_camera_interaction_flags(&self) -> &[bool] {
465 &self.axes_2d_camera_user_controlled
466 }
467
468 fn clear_axes_camera_interaction(&mut self, axes_index: usize) {
469 if let Some(flag) = self.axes_2d_camera_user_controlled.get_mut(axes_index) {
470 *flag = false;
471 }
472 }
473
474 fn clear_all_axes_camera_interaction(&mut self) {
475 for flag in &mut self.axes_2d_camera_user_controlled {
476 *flag = false;
477 }
478 }
479
480 pub fn set_figure(&mut self, figure: Figure) {
482 self.last_geometry_scene_key = None;
483 self.geometry_overlay = None;
484 self.geometry_xray_enabled = false;
485 self.geometry_node_owner_ids.clear();
486 self.geometry_hidden_owner_node_ids.clear();
487 self.scene.clear();
489 self.scene_buffer_cache.borrow_mut().clear();
490
491 self.cache_figure_meta(&figure);
493 self.last_figure = Some(figure.clone());
494 self.last_axes_plot_sizes_px = None;
495 self.last_axes_view_bounds = None;
496 let (rows, cols) = figure.axes_grid();
498 let num_axes = figure.axes_count();
499 let axes_view_contract = Self::axes_view_contract_for_figure(&figure);
500 let axes_view_contract_changed =
501 self.last_axes_view_contract.as_ref() != Some(&axes_view_contract);
502 if axes_view_contract_changed {
503 log::debug!(
504 target: "runmat_plot.camera_refit",
505 "figure axes view contract changed; resetting script-owned camera fit rows={} cols={} axes_count={}",
506 rows,
507 cols,
508 num_axes
509 );
510 self.clear_all_axes_camera_interaction();
511 self.camera_auto_fit = true;
512 }
513 self.last_axes_view_contract = Some(axes_view_contract);
514
515 if self.axes_cameras.len() != num_axes {
516 self.axes_cameras
517 .resize_with(num_axes, Self::create_default_camera);
518 self.axes_2d_camera_user_controlled.resize(num_axes, false);
519 self.axes_applied_view_revisions.resize(num_axes, None);
520 self.camera_auto_fit = true;
521 }
522
523 for axes_index in 0..num_axes {
524 let wants_3d = self.axes_has_3d_content(axes_index);
525 let has_3d_camera = self
526 .axes_cameras
527 .get(axes_index)
528 .map(|cam| {
529 matches!(
530 cam.projection,
531 crate::core::camera::ProjectionType::Perspective { .. }
532 )
533 })
534 .unwrap_or(false);
535 if wants_3d != has_3d_camera {
536 self.axes_cameras[axes_index] = if wants_3d {
537 Camera::new()
538 } else {
539 Self::create_default_camera()
540 };
541 self.clear_axes_camera_interaction(axes_index);
542 if let Some(revision) = self.axes_applied_view_revisions.get_mut(axes_index) {
543 *revision = None;
544 }
545 self.camera_auto_fit = true;
546 }
547 }
548
549 self.add_figure_to_scene(figure);
550
551 self.needs_update = true;
553
554 let fit_applied = if self.camera_auto_fit {
556 if num_axes > 1 {
557 self.fit_cameras_to_axes_data()
558 } else {
559 self.fit_camera_to_data()
560 }
561 } else {
562 false
563 };
564 if self.camera_auto_fit && fit_applied {
565 self.camera_auto_fit = false;
568 }
569 self.apply_stored_axes_views();
570 }
571
572 pub fn set_geometry_scene(&mut self, geometry_scene: GeometryScene) {
578 self.set_geometry_scene_with_presentation(
579 geometry_scene,
580 self.geometry_presentation.clone(),
581 );
582 }
583
584 pub fn set_geometry_scene_with_presentation(
585 &mut self,
586 geometry_scene: GeometryScene,
587 mut presentation: GeometryScenePresentation,
588 ) {
589 let cache_key = geometry_scene.cache_key();
590 let same_scene_identity = self
591 .last_geometry_scene_key
592 .as_ref()
593 .map(|key| key.scene_id.as_str())
594 == Some(cache_key.scene_id.as_str());
595 if same_scene_identity {
596 self.preserve_geometry_scene_state_when_unspecified(&mut presentation);
597 }
598 let preserved_camera = same_scene_identity
599 .then(|| self.axes_cameras.first().cloned())
600 .flatten();
601 let preserved_xray_enabled = same_scene_identity.then_some(self.geometry_xray_enabled);
602 self.geometry_overlay = geometry_scene.overlay.clone();
603 let geometry_xray_enabled = geometry_scene.chunks.iter().any(|chunk| {
604 chunk.material.albedo.w < 0.95
605 || matches!(chunk.material.alpha_mode, crate::core::AlphaMode::Blend)
606 });
607 if self.last_geometry_scene_key.as_ref() == Some(&cache_key) {
608 self.last_geometry_scene = Some(geometry_scene.clone());
609 if self.geometry_presentation != presentation {
610 self.geometry_presentation = presentation;
611 self.refresh_geometry_scene_render_data(&geometry_scene);
612 }
613 return;
614 }
615
616 self.scene.clear();
617 self.scene_buffer_cache.borrow_mut().clear();
618 self.geometry_node_owner_ids.clear();
619 if !same_scene_identity {
620 self.geometry_hidden_owner_node_ids.clear();
621 }
622 self.last_geometry_scene_key = Some(cache_key);
623 self.last_geometry_scene = Some(geometry_scene.clone());
624 self.geometry_presentation = presentation;
625 self.geometry_xray_enabled = preserved_xray_enabled.unwrap_or(geometry_xray_enabled);
626 self.last_figure = None;
627 self.last_scene_viewport_px = Some((
628 self.wgpu_renderer.surface_config.width.max(1),
629 self.wgpu_renderer.surface_config.height.max(1),
630 ));
631 self.last_axes_plot_sizes_px = None;
632 self.last_axes_view_bounds = None;
633 self.last_axes_view_contract = None;
634
635 self.figure_title = geometry_scene.title.clone();
636 self.figure_sg_title = None;
637 self.figure_sg_title_style = TextStyle::default();
638 self.figure_x_label = Some("X".to_string());
639 self.figure_y_label = Some("Y".to_string());
640 self.figure_z_label = Some("Z".to_string());
641 self.figure_show_grid = geometry_scene.show_grid;
642 self.figure_show_legend = false;
643 self.figure_show_box = true;
644 self.figure_x_limits = None;
645 self.figure_y_limits = None;
646 self.legend_entries.clear();
647 self.figure_x_log = false;
648 self.figure_y_log = false;
649 self.figure_axis_equal = geometry_scene.axis_equal;
650 self.figure_colorbar_enabled = false;
651 self.figure_categorical_is_x = None;
652 self.figure_categorical_labels = None;
653
654 self.axes_cameras.clear();
655 self.axes_cameras
656 .push(preserved_camera.clone().unwrap_or_else(Camera::new));
657 self.axes_2d_camera_user_controlled.clear();
658 self.axes_2d_camera_user_controlled.push(false);
659 self.axes_applied_view_revisions.clear();
660 self.axes_applied_view_revisions.push(None);
661
662 self.rebuild_geometry_scene_nodes(&geometry_scene);
663
664 if preserved_camera.is_none() {
665 let mut camera = Camera::new();
666 if Self::bounds_are_finite(geometry_scene.bounds) {
667 camera.target = geometry_scene.bounds.center();
668 camera.up = Vec3::Z;
669 camera.position = camera.target + Vec3::new(1.0, -1.0, 1.0);
670 camera.fit_bounds(geometry_scene.bounds.min, geometry_scene.bounds.max);
671 }
672 self.axes_cameras[0] = camera;
673 }
674 self.camera_auto_fit = false;
675 self.needs_update = true;
676 }
677
678 fn add_figure_to_scene(&mut self, figure: Figure) {
680 self.add_figure_to_scene_with_axes_plot_sizes(figure, None);
681 }
682
683 fn add_figure_to_scene_with_axes_plot_sizes(
684 &mut self,
685 mut figure: Figure,
686 axes_plot_sizes_px: Option<&[(u32, u32)]>,
687 ) {
688 use crate::core::SceneNode;
689
690 let (rows, cols) = figure.axes_grid();
691
692 let viewport_px = (
696 self.wgpu_renderer.surface_config.width.max(1),
697 self.wgpu_renderer.surface_config.height.max(1),
698 );
699 self.last_scene_viewport_px = Some(viewport_px);
700 let gpu = crate::core::GpuPackContext {
701 device: &self.wgpu_renderer.device,
702 queue: &self.wgpu_renderer.queue,
703 };
704 let axes_count = figure.axes_count();
705 let view_bounds = self.axes_view_bounds_for_count(axes_count);
706 let viewport_hint = if axes_plot_sizes_px.is_some() || rows.max(1) * cols.max(1) <= 1 {
707 Some(viewport_px)
708 } else {
709 None
710 };
711 let render_data_list = figure.render_data_with_axes_with_viewport_and_gpu(
712 viewport_hint,
713 axes_plot_sizes_px,
714 Some(&view_bounds),
715 Some(&gpu),
716 );
717
718 for (node_id_counter, (axes_index, render_data)) in render_data_list.into_iter().enumerate()
719 {
720 let axes_index = axes_index.min(axes_count.saturating_sub(1));
721 let node = SceneNode {
723 id: node_id_counter as u64,
724 name: format!("Plot {node_id_counter} @axes {axes_index}"),
725 transform: Mat4::IDENTITY,
726 visible: true,
727 cast_shadows: false,
728 receive_shadows: false,
729 axes_index,
730 parent: None,
731 children: Vec::new(),
732 render_data: Some(render_data),
733 bounds: crate::core::BoundingBox::default(),
734 lod_levels: Vec::new(),
735 current_lod: 0,
736 };
737
738 let nid = self.scene.add_node(node);
739 let _ = nid;
741 let _ = axes_index;
742 let _ = rows;
743 let _ = cols;
744 }
745 }
746
747 pub fn ensure_scene_viewport_dependent_geometry_for_axes(
748 &mut self,
749 axes_plot_sizes_px: &[(u32, u32)],
750 ) {
751 let mut normalized: Vec<(u32, u32)> = axes_plot_sizes_px
752 .iter()
753 .map(|&(w, h)| (w.max(1), h.max(1)))
754 .collect();
755 let Some(figure) = self.last_figure.clone() else {
756 let view_bounds = self.axes_view_bounds_for_count(normalized.len().max(1));
757 self.last_axes_plot_sizes_px = Some(normalized);
758 self.last_axes_view_bounds = Some(view_bounds);
759 return;
760 };
761 let axes_count = figure.axes_count();
762 if normalized.len() < axes_count {
763 let grid_len = normalized.len().max(1);
764 for axes_index in normalized.len()..axes_count {
765 let parent = figure
766 .axes_overlay_parent(axes_index)
767 .unwrap_or(axes_index)
768 .min(grid_len - 1);
769 let size = normalized.get(parent).copied().unwrap_or((
770 self.wgpu_renderer.surface_config.width.max(1),
771 self.wgpu_renderer.surface_config.height.max(1),
772 ));
773 normalized.push(size);
774 }
775 }
776 if normalized.iter().any(|&(w, h)| w < 2 || h < 2) {
777 log::debug!(
778 target: "runmat_plot.viewport_rebuild",
779 "skipped viewport-dependent scene geometry rebuild for unstable viewport_sizes={:?}",
780 normalized
781 );
782 return;
783 }
784 let view_bounds = self.axes_view_bounds_for_count(axes_count);
785 if self.last_axes_plot_sizes_px.as_ref() == Some(&normalized)
786 && self.last_axes_view_bounds.as_ref() == Some(&view_bounds)
787 {
788 return;
789 }
790 self.scene.clear();
791 self.scene_buffer_cache.borrow_mut().clear();
792 self.add_figure_to_scene_with_axes_plot_sizes(figure, Some(&normalized));
793 log::debug!(
794 target: "runmat_plot.viewport_rebuild",
795 "rebuilt viewport-dependent scene geometry axes_count={} viewport_sizes={:?}",
796 normalized.len(),
797 normalized
798 );
799 self.refit_2d_cameras_to_scene_bounds();
800 self.last_axes_plot_sizes_px = Some(normalized);
801 self.last_axes_view_bounds = Some(view_bounds);
802 self.needs_update = true;
803 }
804
805 fn axes_view_bounds_for_count(&self, axes_count: usize) -> PerAxesViewBounds {
806 (0..axes_count)
807 .map(|idx| self.view_bounds_for_axes(idx))
808 .collect()
809 }
810
811 fn direct_log_flags_for_axes(&self, axes_index: usize) -> [u32; 2] {
812 [
813 u32::from(self.overlay_x_log_for_axes(axes_index)),
814 u32::from(self.overlay_y_log_for_axes(axes_index)),
815 ]
816 }
817
818 fn refit_2d_cameras_to_scene_bounds(&mut self) {
819 for idx in 0..self.axes_cameras.len() {
820 if self.axes_has_3d_content(idx) {
821 continue;
822 }
823 if self
824 .axes_2d_camera_user_controlled
825 .get(idx)
826 .copied()
827 .unwrap_or(false)
828 {
829 continue;
830 }
831 let Some((x_min, x_max, y_min, y_max)) = self.display_bounds_for_axes(idx) else {
832 continue;
833 };
834 let geometry_bounds = self.axes_bounds(idx);
835 let Some(cam) = self.axes_cameras.get_mut(idx) else {
836 continue;
837 };
838 if let crate::core::camera::ProjectionType::Orthographic {
839 ref mut left,
840 ref mut right,
841 ref mut bottom,
842 ref mut top,
843 ..
844 } = cam.projection
845 {
846 *left = x_min as f32;
847 *right = x_max as f32;
848 *bottom = y_min as f32;
849 *top = y_max as f32;
850 let camera_left = *left;
851 let camera_right = *right;
852 let camera_bottom = *bottom;
853 let camera_top = *top;
854 cam.position.z = 1.0;
855 cam.target.z = 0.0;
856 cam.mark_dirty();
857 if let Some(bounds) = geometry_bounds {
858 log::debug!(
859 target: "runmat_plot.camera_refit",
860 "refit 2d camera to rebuilt scene bounds axes_index={} geometry=({}, {})..({}, {}) camera=({}, {})..({}, {}) margins=top:{} bottom:{} left:{} right:{}",
861 idx,
862 bounds.min.x,
863 bounds.min.y,
864 bounds.max.x,
865 bounds.max.y,
866 camera_left,
867 camera_bottom,
868 camera_right,
869 camera_top,
870 camera_top - bounds.max.y,
871 bounds.min.y - camera_bottom,
872 bounds.min.x - camera_left,
873 camera_right - bounds.max.x
874 );
875 } else {
876 log::debug!(
877 target: "runmat_plot.camera_refit",
878 "refit 2d camera without geometry bounds axes_index={} camera=({}, {})..({}, {})",
879 idx,
880 camera_left,
881 camera_bottom,
882 camera_right,
883 camera_top
884 );
885 }
886 if let Some(display_bounds) = self.display_bounds_for_axes(idx) {
887 log::debug!(
888 target: "runmat_plot.bounds_chain",
889 "bounds chain axes_index={} axes_bounds=({}, {})..({}, {}) display_bounds=({}, {})..({}, {}) camera_bounds=({}, {})..({}, {})",
890 idx,
891 geometry_bounds.map(|b| b.min.x as f64).unwrap_or(f64::NAN),
892 geometry_bounds.map(|b| b.min.y as f64).unwrap_or(f64::NAN),
893 geometry_bounds.map(|b| b.max.x as f64).unwrap_or(f64::NAN),
894 geometry_bounds.map(|b| b.max.y as f64).unwrap_or(f64::NAN),
895 display_bounds.0,
896 display_bounds.2,
897 display_bounds.1,
898 display_bounds.3,
899 camera_left,
900 camera_bottom,
901 camera_right,
902 camera_top
903 );
904 }
905 }
906 }
907 }
908
909 fn cache_figure_meta(&mut self, figure: &Figure) {
911 self.figure_title = figure.title.clone();
912 self.figure_sg_title = figure.sg_title.clone();
913 self.figure_sg_title_style = figure.sg_title_style.clone();
914 self.figure_x_label = figure.x_label.clone();
915 self.figure_y_label = figure.y_label.clone();
916 self.figure_z_label = figure.z_label.clone();
917 self.figure_show_grid = figure.grid_enabled;
918 self.figure_show_minor_grid = figure.minor_grid_enabled;
919 self.figure_show_legend = figure.legend_enabled;
920 self.figure_show_box = figure.box_enabled;
921 self.figure_x_limits = figure.x_limits;
922 self.figure_y_limits = figure.y_limits;
923 self.legend_entries = figure.legend_entries();
924 self.figure_x_log = figure.x_log;
925 self.figure_y_log = figure.y_log;
926 self.figure_axis_equal = figure.axis_equal;
927 self.figure_colormap = figure.colormap.clone();
928 self.figure_colorbar_enabled = figure.colorbar_enabled;
929 if let Some((is_x, labels)) = figure.categorical_axis_labels() {
931 self.figure_categorical_is_x = Some(is_x);
932 self.figure_categorical_labels = Some(labels);
933 } else {
934 self.figure_categorical_is_x = None;
935 self.figure_categorical_labels = None;
936 }
937 }
938
939 fn apply_stored_axes_views(&mut self) {
940 let Some(fig) = self.last_figure.as_ref() else {
941 return;
942 };
943 for (idx, cam) in self.axes_cameras.iter_mut().enumerate() {
944 if !matches!(
945 cam.projection,
946 crate::core::camera::ProjectionType::Perspective { .. }
947 ) {
948 continue;
949 }
950 if let Some(meta) = fig.axes_metadata(idx) {
951 if let (Some(az), Some(el)) = (meta.view_azimuth_deg, meta.view_elevation_deg) {
952 if self.axes_applied_view_revisions.get(idx).copied().flatten()
953 == Some(meta.view_revision)
954 {
955 continue;
956 }
957 cam.set_view_angles_deg(az, el);
958 if let Some(revision) = self.axes_applied_view_revisions.get_mut(idx) {
959 *revision = Some(meta.view_revision);
960 }
961 } else if let Some(revision) = self.axes_applied_view_revisions.get_mut(idx) {
962 *revision = None;
963 }
964 }
965 }
966 }
967
968 fn display_bounds_for_axes(&self, axes_index: usize) -> Option<(f64, f64, f64, f64)> {
969 let base = self.axes_bounds(axes_index)?;
970 let mut x_min = base.min.x as f64;
971 let mut x_max = base.max.x as f64;
972 let mut y_min = base.min.y as f64;
973 let mut y_max = base.max.y as f64;
974
975 if let Some(fig) = self.last_figure.as_ref() {
976 if let Some(meta) = fig.axes_metadata(axes_index) {
977 if let Some((xl, xr)) = meta.x_limits {
978 x_min = xl;
979 x_max = xr;
980 }
981 if let Some((yl, yr)) = meta.y_limits {
982 y_min = yl;
983 y_max = yr;
984 }
985 if meta.axis_equal || meta.data_aspect_ratio_mode == "manual" {
986 (x_min, x_max, y_min, y_max) = data_aspect_adjusted_bounds(
987 x_min,
988 x_max,
989 y_min,
990 y_max,
991 meta.data_aspect_ratio,
992 );
993 }
994 }
995 }
996 if let Some(meta) = self
997 .last_figure
998 .as_ref()
999 .and_then(|fig| fig.axes_metadata(axes_index))
1000 {
1001 if meta.x_log {
1002 (x_min, x_max) = Self::positive_log_bounds(x_min, x_max);
1003 }
1004 if meta.y_log {
1005 (y_min, y_max) = Self::positive_log_bounds(y_min, y_max);
1006 }
1007 }
1008
1009 Some((x_min, x_max, y_min, y_max))
1010 }
1011
1012 fn positive_log_bounds(lo: f64, hi: f64) -> (f64, f64) {
1013 if lo.is_finite() && hi.is_finite() && lo > 0.0 && hi > 0.0 && lo != hi {
1014 return (lo, hi);
1015 }
1016 let upper = if hi.is_finite() && hi > 0.0 {
1017 hi
1018 } else if lo.is_finite() && lo > 0.0 {
1019 lo
1020 } else {
1021 1.0
1022 };
1023 let lower = (upper / 1000.0).max(f64::MIN_POSITIVE);
1024 if lower < upper {
1025 (lower, upper)
1026 } else {
1027 (upper * 0.1, upper * 10.0)
1028 }
1029 }
1030
1031 fn apply_3d_display_limits_to_bounds(
1032 bounds: BoundingBox,
1033 figure: Option<&Figure>,
1034 axes_index: usize,
1035 ) -> BoundingBox {
1036 let Some(meta) = figure.and_then(|fig| fig.axes_metadata(axes_index)) else {
1037 return bounds;
1038 };
1039 let mut min = bounds.min;
1040 let mut max = bounds.max;
1041 if let Some((lo, hi)) = meta.x_limits {
1042 min.x = lo as f32;
1043 max.x = hi as f32;
1044 }
1045 if let Some((lo, hi)) = meta.y_limits {
1046 min.y = lo as f32;
1047 max.y = hi as f32;
1048 }
1049 if let Some((lo, hi)) = meta.z_limits {
1050 min.z = lo as f32;
1051 max.z = hi as f32;
1052 }
1053 if meta.axis_equal || meta.data_aspect_ratio_mode == "manual" {
1054 (min, max) = data_aspect_adjusted_bounds_3d(min, max, meta.data_aspect_ratio);
1055 }
1056 BoundingBox { min, max }
1057 }
1058
1059 fn bounds_are_finite(bounds: BoundingBox) -> bool {
1060 bounds.min.x.is_finite()
1061 && bounds.min.y.is_finite()
1062 && bounds.min.z.is_finite()
1063 && bounds.max.x.is_finite()
1064 && bounds.max.y.is_finite()
1065 && bounds.max.z.is_finite()
1066 }
1067
1068 fn current_3d_display_bounds_for_axes(&self, axes_index: usize) -> Option<BoundingBox> {
1069 let bounds = self.axes_bounds(axes_index)?;
1070 let bounds =
1071 Self::apply_3d_display_limits_to_bounds(bounds, self.last_figure.as_ref(), axes_index);
1072 Self::bounds_are_finite(bounds).then_some(bounds)
1073 }
1074
1075 fn display_bounds_3d_for_axes(&self, axes_index: usize) -> Option<BoundingBox> {
1076 self.current_3d_display_bounds_for_axes(axes_index)
1077 }
1078
1079 fn axes_model_matrix(&self, _axes_index: usize) -> Mat4 {
1080 Mat4::IDENTITY
1081 }
1082
1083 fn fit_cameras_to_axes_data(&mut self) -> bool {
1084 let mut applied = false;
1085 for idx in 0..self.axes_cameras.len() {
1086 if self.axes_has_3d_content(idx) {
1087 let Some(bounds) = self.display_bounds_3d_for_axes(idx) else {
1088 continue;
1089 };
1090 let center = (bounds.min + bounds.max) * 0.5;
1091 let mut cam = Camera::new();
1092 cam.target = center;
1093 cam.up = Vec3::Z;
1094 cam.position = center + Vec3::new(1.0, -1.0, 1.0);
1095 cam.fit_bounds(bounds.min, bounds.max);
1096 self.axes_cameras[idx] = cam;
1097 applied = true;
1098 continue;
1099 }
1100
1101 let Some((x_min, x_max, y_min, y_max)) = self.display_bounds_for_axes(idx) else {
1102 continue;
1103 };
1104 let mut cam = Self::create_default_camera();
1105 if let crate::core::camera::ProjectionType::Orthographic {
1106 ref mut left,
1107 ref mut right,
1108 ref mut bottom,
1109 ref mut top,
1110 ..
1111 } = cam.projection
1112 {
1113 *left = x_min as f32;
1114 *right = x_max as f32;
1115 *bottom = y_min as f32;
1116 *top = y_max as f32;
1117 }
1118 cam.position.z = 1.0;
1119 cam.target.z = 0.0;
1120 cam.mark_dirty();
1121 self.axes_cameras[idx] = cam;
1122 applied = true;
1123 }
1124 applied
1125 }
1126
1127 pub fn calculate_data_bounds(&mut self) -> Option<(f64, f64, f64, f64)> {
1129 let mut min_x = f64::INFINITY;
1130 let mut max_x = f64::NEG_INFINITY;
1131 let mut min_y = f64::INFINITY;
1132 let mut max_y = f64::NEG_INFINITY;
1133
1134 for node in self.scene.get_visible_nodes() {
1135 if let Some(render_data) = &node.render_data {
1136 if let Some(bounds) = render_data.bounds {
1137 min_x = min_x.min(bounds.min.x as f64);
1138 max_x = max_x.max(bounds.max.x as f64);
1139 min_y = min_y.min(bounds.min.y as f64);
1140 max_y = max_y.max(bounds.max.y as f64);
1141 continue;
1142 }
1143 for vertex in &render_data.vertices {
1144 let x = vertex.position[0] as f64;
1145 let y = vertex.position[1] as f64;
1146 min_x = min_x.min(x);
1147 max_x = max_x.max(x);
1148 min_y = min_y.min(y);
1149 max_y = max_y.max(y);
1150 }
1151 }
1152 }
1153
1154 if min_x != f64::INFINITY && max_x != f64::NEG_INFINITY {
1155 let x_range = (max_x - min_x).max(0.1);
1158 let y_range = (max_y - min_y).max(0.1);
1159 let x_margin = x_range * 0.04;
1160 let y_margin = y_range * 0.04;
1161
1162 let bounds = (
1163 min_x - x_margin,
1164 max_x + x_margin,
1165 min_y - y_margin,
1166 max_y + y_margin,
1167 );
1168
1169 self.data_bounds = Some(bounds);
1171 Some(bounds)
1172 } else {
1173 self.data_bounds = None;
1174 None
1175 }
1176 }
1177
1178 pub fn fit_camera_to_data(&mut self) -> bool {
1182 if self.axes_cameras.len() > 1 {
1183 return self.fit_cameras_to_axes_data();
1184 }
1185
1186 if self.axes_has_3d_content(0) {
1187 let Some(bounds) = self.display_bounds_3d_for_axes(0) else {
1188 return false;
1189 };
1190 let center = (bounds.min + bounds.max) * 0.5;
1191 let mut cam = Camera::new();
1192 cam.target = center;
1193 cam.up = Vec3::Z;
1194 cam.position = center + Vec3::new(1.0, -1.0, 1.0);
1195 cam.fit_bounds(bounds.min, bounds.max);
1196 if let Some(axis_cam) = self.axes_cameras.get_mut(0) {
1197 *axis_cam = cam;
1198 }
1199 return true;
1200 }
1201
1202 if let Some((x_min, x_max, y_min, y_max)) = self.display_bounds_for_axes(0) {
1203 let mut cam = Self::create_default_camera();
1205 let l = x_min as f32;
1206 let r = x_max as f32;
1207 let b = y_min as f32;
1208 let t = y_max as f32;
1209 if let crate::core::camera::ProjectionType::Orthographic {
1210 ref mut left,
1211 ref mut right,
1212 ref mut bottom,
1213 ref mut top,
1214 ..
1215 } = cam.projection
1216 {
1217 *left = l;
1218 *right = r;
1219 *bottom = b;
1220 *top = t;
1221 }
1222 cam.position.z = 1.0;
1223 cam.target.z = 0.0;
1224 cam.mark_dirty();
1225
1226 if let Some(axis_cam) = self.axes_cameras.get_mut(0) {
1227 *axis_cam = cam;
1228 }
1229 return true;
1230 }
1231 false
1232 }
1233
1234 pub fn fit_extents(&mut self) {
1236 let _ = if self.figure_axes_grid().0 * self.figure_axes_grid().1 > 1 {
1237 self.fit_cameras_to_axes_data()
1238 } else {
1239 self.fit_camera_to_data()
1240 };
1241 self.clear_all_axes_camera_interaction();
1242 self.camera_auto_fit = false;
1243 self.needs_update = true;
1244 }
1245
1246 pub fn reset_geometry_view(&mut self) {
1247 self.set_camera_view_preset(CameraViewPreset::Perspective);
1248 }
1249
1250 pub fn set_camera_view_preset(&mut self, preset: CameraViewPreset) {
1251 let bounds_by_axes: Vec<Option<BoundingBox>> = (0..self.axes_cameras.len())
1252 .map(|idx| {
1253 if self.axes_has_3d_content(idx) {
1254 self.display_bounds_3d_for_axes(idx)
1255 } else {
1256 self.axes_bounds(idx)
1257 }
1258 })
1259 .collect();
1260 let display_bounds: PerAxesViewBounds = (0..self.axes_cameras.len())
1261 .map(|idx| self.display_bounds_for_axes(idx))
1262 .collect();
1263
1264 for (idx, camera) in self.axes_cameras.iter_mut().enumerate() {
1265 if matches!(
1266 camera.projection,
1267 crate::core::camera::ProjectionType::Perspective { .. }
1268 ) {
1269 Self::apply_perspective_view_preset(camera, preset, bounds_by_axes[idx]);
1270 } else if let Some((x_min, x_max, y_min, y_max)) = display_bounds[idx] {
1271 let mut cam = Self::create_default_camera();
1272 if let crate::core::camera::ProjectionType::Orthographic {
1273 ref mut left,
1274 ref mut right,
1275 ref mut bottom,
1276 ref mut top,
1277 ..
1278 } = cam.projection
1279 {
1280 *left = x_min as f32;
1281 *right = x_max as f32;
1282 *bottom = y_min as f32;
1283 *top = y_max as f32;
1284 }
1285 cam.position.z = 1.0;
1286 cam.target.z = 0.0;
1287 cam.mark_dirty();
1288 *camera = cam;
1289 }
1290 }
1291
1292 self.clear_all_axes_camera_interaction();
1293 self.camera_auto_fit = false;
1294 self.needs_update = true;
1295 }
1296
1297 fn apply_perspective_view_preset(
1298 camera: &mut Camera,
1299 preset: CameraViewPreset,
1300 bounds: Option<BoundingBox>,
1301 ) {
1302 let (direction, up) = Self::view_preset_orientation(preset);
1303 camera.up = up;
1304
1305 if let Some(bounds) = bounds {
1306 let center = (bounds.min + bounds.max) * 0.5;
1307 camera.target = center;
1308 camera.position = center + direction;
1309 camera.fit_bounds(bounds.min, bounds.max);
1310 } else {
1311 let distance = (camera.position - camera.target).length().max(1.0);
1312 camera.position = camera.target + direction * distance;
1313 camera.mark_dirty();
1314 }
1315 }
1316
1317 fn view_preset_orientation(preset: CameraViewPreset) -> (Vec3, Vec3) {
1318 match preset {
1319 CameraViewPreset::Perspective => (Vec3::new(1.0, -1.0, 1.0).normalize(), Vec3::Z),
1320 CameraViewPreset::Top => (Vec3::Z, Vec3::Y),
1321 CameraViewPreset::Bottom => (-Vec3::Z, Vec3::Y),
1322 CameraViewPreset::Front => (-Vec3::Y, Vec3::Z),
1323 CameraViewPreset::Back => (Vec3::Y, Vec3::Z),
1324 CameraViewPreset::Left => (-Vec3::X, Vec3::Z),
1325 CameraViewPreset::Right => (Vec3::X, Vec3::Z),
1326 }
1327 }
1328
1329 pub fn reset_camera_position(&mut self) {
1335 let dir = Vec3::new(1.0, -1.0, 1.0).normalize_or_zero();
1336 let data_centers: Vec<Vec3> = (0..self.axes_cameras.len())
1337 .map(|idx| {
1338 if self.axes_has_3d_content(idx) {
1339 self.display_bounds_3d_for_axes(idx)
1340 } else {
1341 self.axes_bounds(idx)
1342 }
1343 .map(|b| (b.min + b.max) * 0.5)
1344 .unwrap_or_else(|| self.axes_cameras[idx].target)
1345 })
1346 .collect();
1347 let display_bounds: PerAxesViewBounds = (0..self.axes_cameras.len())
1348 .map(|idx| self.display_bounds_for_axes(idx))
1349 .collect();
1350 for (idx, c) in self.axes_cameras.iter_mut().enumerate() {
1351 if matches!(
1352 c.projection,
1353 crate::core::camera::ProjectionType::Perspective { .. }
1354 ) {
1355 let data_center = data_centers.get(idx).copied().unwrap_or(c.target);
1356 let dist = (c.position - c.target).length().max(0.1);
1357 c.target = data_center;
1358 c.up = Vec3::Z;
1359 c.position = data_center + dir * dist;
1360 c.mark_dirty();
1361 } else if let Some((x_min, x_max, y_min, y_max)) = display_bounds[idx] {
1362 let mut cam = Self::create_default_camera();
1363 if let crate::core::camera::ProjectionType::Orthographic {
1364 ref mut left,
1365 ref mut right,
1366 ref mut bottom,
1367 ref mut top,
1368 ..
1369 } = cam.projection
1370 {
1371 *left = x_min as f32;
1372 *right = x_max as f32;
1373 *bottom = y_min as f32;
1374 *top = y_max as f32;
1375 }
1376 cam.position.z = 1.0;
1377 cam.target.z = 0.0;
1378 cam.mark_dirty();
1379 *c = cam;
1380 }
1381 }
1382 self.clear_all_axes_camera_interaction();
1383 self.camera_auto_fit = false;
1384 self.needs_update = true;
1385 }
1386
1387 pub fn render_to_viewport(
1389 &mut self,
1390 encoder: &mut wgpu::CommandEncoder,
1391 target_view: &wgpu::TextureView,
1392 _viewport: (f32, f32, f32, f32), clear_background: bool,
1394 background_color: Option<glam::Vec4>,
1395 ) -> Result<RenderResult, Box<dyn std::error::Error>> {
1396 let start_time = Instant::now();
1397
1398 let mut render_items = Vec::new();
1400 let mut total_vertices = 0;
1401 let mut total_triangles = 0;
1402
1403 for node in self.scene.get_visible_nodes() {
1404 if let Some(render_data) = &node.render_data {
1405 if let Some(vertex_buffer) = self.wgpu_renderer.vertex_buffer_from_sources(
1406 render_data.gpu_vertices.as_ref(),
1407 &render_data.vertices,
1408 ) {
1409 self.wgpu_renderer
1410 .ensure_pipeline(render_data.pipeline_type);
1411
1412 log::trace!(
1413 target: "runmat_plot",
1414 "upload vertices={}, draw_calls={}",
1415 render_data.vertex_count(),
1416 render_data.draw_calls.len()
1417 );
1418
1419 render_items.push((render_data, vertex_buffer));
1420 total_vertices += render_data.vertex_count();
1421
1422 if render_data.pipeline_type == crate::core::PipelineType::Triangles {
1423 total_triangles += render_data.vertex_count() / 3;
1424 }
1425 }
1426 }
1427 }
1428
1429 let mut cam = self.camera().clone();
1431 let view_proj_matrix = cam.view_proj_matrix();
1432
1433 self.wgpu_renderer
1434 .update_uniforms(view_proj_matrix, Mat4::IDENTITY);
1435
1436 let use_msaa = self.wgpu_renderer.msaa_sample_count > 1;
1438 let msaa_view_opt = if use_msaa {
1439 let tex = self
1440 .wgpu_renderer
1441 .device
1442 .create_texture(&wgpu::TextureDescriptor {
1443 label: Some("runmat_msaa_color_camera"),
1444 size: wgpu::Extent3d {
1445 width: self.wgpu_renderer.surface_config.width,
1446 height: self.wgpu_renderer.surface_config.height,
1447 depth_or_array_layers: 1,
1448 },
1449 mip_level_count: 1,
1450 sample_count: self.wgpu_renderer.msaa_sample_count,
1451 dimension: wgpu::TextureDimension::D2,
1452 format: self.wgpu_renderer.surface_config.format,
1453 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
1454 view_formats: &[],
1455 });
1456 Some(tex.create_view(&wgpu::TextureViewDescriptor::default()))
1457 } else {
1458 None
1459 };
1460
1461 let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1462 label: Some("Viewport Plot Render Pass"),
1463 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1464 view: msaa_view_opt.as_ref().unwrap_or(target_view),
1465 resolve_target: if use_msaa { Some(target_view) } else { None },
1466 ops: wgpu::Operations {
1467 load: if clear_background {
1468 wgpu::LoadOp::Clear(wgpu::Color {
1469 r: background_color.map_or(0.08, |c| c.x as f64),
1470 g: background_color.map_or(0.09, |c| c.y as f64),
1471 b: background_color.map_or(0.11, |c| c.z as f64),
1472 a: background_color.map_or(1.0, |c| c.w as f64),
1473 })
1474 } else {
1475 wgpu::LoadOp::Load
1476 },
1477 store: wgpu::StoreOp::Store,
1478 },
1479 })],
1480 depth_stencil_attachment: None,
1481 occlusion_query_set: None,
1482 timestamp_writes: None,
1483 });
1484
1485 let (vx, vy, vw, vh) = _viewport;
1487 render_pass.set_viewport(vx, vy, vw, vh, 0.0, 1.0);
1488
1489 let sw = self.wgpu_renderer.surface_config.width as f32;
1491 let sh = self.wgpu_renderer.surface_config.height as f32;
1492 let ndc_left = (vx / sw) * 2.0 - 1.0;
1493 let ndc_right = ((vx + vw) / sw) * 2.0 - 1.0;
1494 let ndc_top = 1.0 - (vy / sh) * 2.0;
1495 let ndc_bottom = 1.0 - ((vy + vh) / sh) * 2.0;
1496
1497 let (x_min, y_min, x_max, y_max) = (0.0_f64, 0.0_f64, 1.0_f64, 1.0_f64);
1499 self.wgpu_renderer
1500 .update_direct_uniforms(DirectUniforms::new(
1501 [x_min as f32, y_min as f32],
1502 [x_max as f32, y_max as f32],
1503 [ndc_left, ndc_bottom],
1504 [ndc_right, ndc_top],
1505 [sw, sh],
1506 [0, 0],
1507 ));
1508
1509 drop(render_pass);
1511
1512 let render_time = start_time.elapsed().as_secs_f64() * 1000.0;
1513
1514 Ok(RenderResult {
1515 success: true,
1516 data_bounds: self.data_bounds,
1517 axes_viewports_px: vec![(
1518 vx.round().max(0.0) as u32,
1519 vy.round().max(0.0) as u32,
1520 vw.round().max(1.0) as u32,
1521 vh.round().max(1.0) as u32,
1522 )],
1523 vertex_count: total_vertices,
1524 triangle_count: total_triangles,
1525 render_time_ms: render_time,
1526 })
1527 }
1528
1529 pub fn render(
1531 &mut self,
1532 encoder: &mut wgpu::CommandEncoder,
1533 target: RenderTarget<'_>,
1534 config: &PlotRenderConfig,
1535 ) -> Result<RenderResult, Box<dyn std::error::Error>> {
1536 let start_time = Instant::now();
1537
1538 self.wgpu_renderer.ensure_msaa(config.msaa_samples);
1539
1540 let aspect_ratio = config.width as f32 / config.height as f32;
1542 let mut cam = self.camera().clone();
1543 cam.update_aspect_ratio(aspect_ratio);
1544 let view_proj_matrix = cam.view_proj_matrix();
1545 let model_matrix = self.axes_model_matrix(0);
1546 self.wgpu_renderer
1547 .update_uniforms(view_proj_matrix, model_matrix);
1548
1549 let mut render_items = Vec::new();
1551 let mut total_vertices = 0;
1552 let mut total_triangles = 0;
1553
1554 for node in self.scene.get_visible_nodes() {
1555 if let Some(render_data) = &node.render_data {
1556 if let Some((vertex_buffer, index_buffer)) =
1557 self.prepare_buffers_for_render_data(node.id, render_data)
1558 {
1559 self.wgpu_renderer
1560 .ensure_pipeline(render_data.pipeline_type);
1561 render_items.push((render_data, vertex_buffer, index_buffer));
1562
1563 total_vertices += render_data.vertex_count();
1564 if let Some(indices) = &render_data.indices {
1565 total_triangles += indices.len() / 3;
1566 }
1567 }
1568 }
1569 }
1570
1571 let mut image_bind_groups: Vec<Option<wgpu::BindGroup>> =
1573 Vec::with_capacity(render_items.len());
1574 let has_textured_items = render_items.iter().any(|(render_data, _, _)| {
1575 render_data.pipeline_type == crate::core::PipelineType::Textured
1576 });
1577 if has_textured_items {
1578 self.wgpu_renderer.ensure_image_pipeline();
1580 let mut inferred_bounds: Option<(f64, f64, f64, f64)> = None;
1581 for (render_data, _, _) in &render_items {
1582 let Some(bounds) = render_data.bounds.as_ref() else {
1583 continue;
1584 };
1585 let min_x = bounds.min.x as f64;
1586 let max_x = bounds.max.x as f64;
1587 let min_y = bounds.min.y as f64;
1588 let max_y = bounds.max.y as f64;
1589 inferred_bounds = Some(match inferred_bounds {
1590 Some((x0, x1, y0, y1)) => {
1591 (x0.min(min_x), x1.max(max_x), y0.min(min_y), y1.max(max_y))
1592 }
1593 None => (min_x, max_x, min_y, max_y),
1594 });
1595 }
1596
1597 let (mut x_min, mut x_max, mut y_min, mut y_max) = self
1598 .data_bounds
1599 .or(inferred_bounds)
1600 .unwrap_or((-1.0, 1.0, -1.0, 1.0));
1601 if (x_max - x_min).abs() < f64::EPSILON {
1603 x_min -= 0.5;
1604 x_max += 0.5;
1605 }
1606 if (y_max - y_min).abs() < f64::EPSILON {
1607 y_min -= 0.5;
1608 y_max += 0.5;
1609 }
1610 log::trace!(
1611 target: "runmat_plot",
1612 "direct uniforms bounds x=({}, {}) y=({}, {}) size=({}, {})",
1613 x_min,
1614 x_max,
1615 y_min,
1616 y_max,
1617 config.width,
1618 config.height
1619 );
1620 self.wgpu_renderer
1621 .update_direct_uniforms(DirectUniforms::new(
1622 [x_min as f32, y_min as f32],
1623 [x_max as f32, y_max as f32],
1624 [-1.0, -1.0],
1625 [1.0, 1.0],
1626 [config.width as f32, config.height as f32],
1627 [0, 0],
1628 ));
1629 }
1630 for (render_data, _vb, _ib) in &render_items {
1631 if render_data.pipeline_type == crate::core::PipelineType::Textured {
1632 if let Some(crate::core::scene::ImageData::Rgba8 {
1633 width,
1634 height,
1635 data,
1636 }) = &render_data.image
1637 {
1638 let (_tex, _view, img_bg) = self
1639 .wgpu_renderer
1640 .create_image_texture_and_bind_group(*width, *height, data);
1641 image_bind_groups.push(Some(img_bg));
1642 } else {
1643 image_bind_groups.push(None);
1644 }
1645 } else {
1646 image_bind_groups.push(None);
1647 }
1648 }
1649 let mut point_style_bind_groups: Vec<Option<wgpu::BindGroup>> =
1650 Vec::with_capacity(render_items.len());
1651 for (render_data, _vb, _ib) in &render_items {
1652 if matches!(
1653 render_data.pipeline_type,
1654 crate::core::PipelineType::Points | crate::core::PipelineType::Scatter3
1655 ) {
1656 let style = crate::core::renderer::PointStyleUniforms {
1657 face_color: render_data.material.albedo.to_array(),
1658 edge_color: render_data.material.emissive.to_array(),
1659 edge_thickness_px: render_data.material.roughness,
1660 marker_shape: render_data.material.metallic as u32,
1661 _pad: [0.0, 0.0],
1662 };
1663 let (_buf, bg) = self.wgpu_renderer.create_point_style_bind_group(style);
1664 point_style_bind_groups.push(Some(bg));
1665 } else {
1666 point_style_bind_groups.push(None);
1667 }
1668 }
1669 let mut point_buffers: Vec<Option<(wgpu::Buffer, usize)>> =
1671 Vec::with_capacity(render_items.len());
1672 for (render_data, _vb, _ib) in &render_items {
1673 if matches!(
1674 render_data.pipeline_type,
1675 crate::core::PipelineType::Points | crate::core::PipelineType::Scatter3
1676 ) && !render_data.vertices.is_empty()
1677 {
1678 let expanded = self
1679 .wgpu_renderer
1680 .create_direct_point_vertices(&render_data.vertices, 0.0);
1681 let buffer = self.wgpu_renderer.create_vertex_buffer(&expanded);
1682 point_buffers.push(Some((buffer, expanded.len())));
1683 } else {
1684 point_buffers.push(None);
1685 }
1686 }
1687 self.wgpu_renderer.update_marker_screen_uniforms([
1688 config.width.max(1) as f32,
1689 config.height.max(1) as f32,
1690 ]);
1691
1692 {
1694 let depth_view = self.wgpu_renderer.ensure_depth_view();
1695 let use_msaa = self.wgpu_renderer.msaa_sample_count > 1;
1696 let mut cached_msaa_view: Option<Arc<wgpu::TextureView>> = None;
1697
1698 let (color_view, resolve_target) = if use_msaa {
1699 if let Some(explicit_resolve_target) = target.resolve_target {
1700 (target.view, Some(explicit_resolve_target))
1701 } else {
1702 cached_msaa_view = Some(self.wgpu_renderer.ensure_msaa_color_view());
1703 (
1704 cached_msaa_view
1705 .as_ref()
1706 .expect("msaa color view should exist")
1707 .as_ref(),
1708 Some(target.view),
1709 )
1710 }
1711 } else {
1712 (target.view, target.resolve_target)
1713 };
1714
1715 let depth_clear = match self.wgpu_renderer.depth_mode {
1716 crate::core::DepthMode::Standard => 1.0,
1717 crate::core::DepthMode::ReversedZ => 0.0,
1718 };
1719 let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1720 label: Some("Plot Render Pass"),
1721 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1722 view: color_view,
1723 resolve_target,
1724 ops: wgpu::Operations {
1725 load: wgpu::LoadOp::Clear(wgpu::Color {
1726 r: config.background_color.x as f64,
1727 g: config.background_color.y as f64,
1728 b: config.background_color.z as f64,
1729 a: config.background_color.w as f64,
1730 }),
1731 store: wgpu::StoreOp::Store,
1732 },
1733 })],
1734 depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
1735 view: &depth_view,
1736 depth_ops: Some(wgpu::Operations {
1737 load: wgpu::LoadOp::Clear(depth_clear),
1738 store: wgpu::StoreOp::Discard,
1739 }),
1740 stencil_ops: None,
1741 }),
1742 occlusion_query_set: None,
1743 timestamp_writes: None,
1744 });
1745 let _keep_msaa_view_alive = &cached_msaa_view;
1746
1747 for (i, (render_data, vertex_buffer, index_buffer)) in render_items.iter().enumerate() {
1749 #[cfg(target_arch = "wasm32")]
1750 {
1751 if log::log_enabled!(log::Level::Debug) {
1754 if let Some(v0) = render_data.vertices.first() {
1755 log::debug!(
1756 target: "runmat_plot",
1757 "wasm draw item: pipeline={:?} verts={} v0.pos=({:.3},{:.3},{:.3}) v0.color=({:.3},{:.3},{:.3},{:.3})",
1758 render_data.pipeline_type,
1759 render_data.vertices.len(),
1760 v0.position[0],
1761 v0.position[1],
1762 v0.position[2],
1763 v0.color[0],
1764 v0.color[1],
1765 v0.color[2],
1766 v0.color[3],
1767 );
1768 } else if render_data.gpu_vertices.is_some() {
1769 log::debug!(
1770 target: "runmat_plot",
1771 "wasm draw item: pipeline={:?} using gpu_vertices vertex_count={}",
1772 render_data.pipeline_type,
1773 render_data.vertex_count(),
1774 );
1775 } else {
1776 log::debug!(
1777 target: "runmat_plot",
1778 "wasm draw item: pipeline={:?} has no vertices",
1779 render_data.pipeline_type
1780 );
1781 }
1782 }
1783 }
1784
1785 if render_data.pipeline_type == crate::core::PipelineType::Textured {
1787 let pipeline = self.wgpu_renderer.get_pipeline(render_data.pipeline_type);
1789 render_pass.set_pipeline(pipeline);
1790 render_pass.set_bind_group(
1793 0,
1794 &self.wgpu_renderer.direct_uniform_bind_group,
1795 &[],
1796 );
1797 if let Some(ref img_bg) = image_bind_groups[i] {
1798 render_pass.set_bind_group(1, img_bg, &[]);
1799 }
1800 } else {
1801 let pipeline = self.wgpu_renderer.get_pipeline(render_data.pipeline_type);
1802 render_pass.set_pipeline(pipeline);
1803 render_pass.set_bind_group(0, self.wgpu_renderer.get_uniform_bind_group(), &[]);
1805 if matches!(
1806 render_data.pipeline_type,
1807 crate::core::PipelineType::Points | crate::core::PipelineType::Scatter3
1808 ) {
1809 if let Some(ref bg) = point_style_bind_groups[i] {
1810 render_pass.set_bind_group(1, bg, &[]);
1811 }
1812 render_pass.set_bind_group(
1813 2,
1814 self.wgpu_renderer.get_marker_screen_bind_group(),
1815 &[],
1816 );
1817 }
1818 }
1819
1820 let is_markers = matches!(
1821 render_data.pipeline_type,
1822 crate::core::PipelineType::Points | crate::core::PipelineType::Scatter3
1823 );
1824 if is_markers {
1825 if let Some((ref expanded, _len)) = point_buffers[i] {
1826 render_pass.set_vertex_buffer(0, expanded.slice(..));
1827 } else {
1828 render_pass.set_vertex_buffer(0, vertex_buffer.slice(..));
1829 }
1830 } else {
1831 render_pass.set_vertex_buffer(0, vertex_buffer.slice(..));
1832 }
1833
1834 if let Some(index_buffer) = index_buffer {
1835 render_pass.set_index_buffer(index_buffer.slice(..), wgpu::IndexFormat::Uint32);
1836 if let Some(indices) = &render_data.indices {
1837 log::trace!(target: "runmat_plot", "draw indexed count={}", indices.len());
1838 render_pass.draw_indexed(0..indices.len() as u32, 0, 0..1);
1839 }
1840 } else {
1841 log::trace!(target: "runmat_plot", "draw direct vertices");
1842 if let Some((args, offset)) = Self::gpu_indirect_args(render_data) {
1843 render_pass.draw_indirect(args, offset);
1844 continue;
1845 }
1846 if is_markers {
1847 if let Some((_, len)) = point_buffers[i] {
1848 render_pass.draw(0..len as u32, 0..1);
1849 continue;
1850 }
1851 }
1852 for draw_call in &render_data.draw_calls {
1854 log::trace!(
1855 target: "runmat_plot",
1856 "draw vertices offset={} count={} instances={}",
1857 draw_call.vertex_offset,
1858 draw_call.vertex_count,
1859 draw_call.instance_count
1860 );
1861 render_pass.draw(
1862 draw_call.vertex_offset as u32
1863 ..(draw_call.vertex_offset + draw_call.vertex_count) as u32,
1864 0..draw_call.instance_count as u32,
1865 );
1866 }
1867 }
1868 }
1869 }
1871
1872 let render_time = start_time.elapsed().as_secs_f64() * 1000.0;
1873
1874 Ok(RenderResult {
1875 success: true,
1876 data_bounds: self.data_bounds,
1877 axes_viewports_px: vec![(0, 0, config.width.max(1), config.height.max(1))],
1878 vertex_count: total_vertices,
1879 triangle_count: total_triangles,
1880 render_time_ms: render_time,
1881 })
1882 }
1883
1884 pub fn render_scene_to_target(
1889 &mut self,
1890 encoder: &mut wgpu::CommandEncoder,
1891 target_view: &wgpu::TextureView,
1892 config: &PlotRenderConfig,
1893 ) -> Result<RenderResult, Box<dyn std::error::Error>> {
1894 let start_time = Instant::now();
1895 let (rows, cols) = self.figure_axes_grid();
1896 let axes_count = self.figure_axes_count();
1897 log::debug!(
1898 "runmat-plot: renderer.scene_to_target.start rows={} cols={} axes_count={} width={} height={}",
1899 rows,
1900 cols,
1901 axes_count,
1902 config.width,
1903 config.height
1904 );
1905 if axes_count <= 1 && !self.axes_position_explicit_for_axes(0) {
1906 log::debug!("runmat-plot: renderer.scene_to_target.branch_single_axes");
1907 return self.render(
1908 encoder,
1909 RenderTarget {
1910 view: target_view,
1911 resolve_target: None,
1912 },
1913 config,
1914 );
1915 }
1916
1917 let viewports = self.compute_axes_viewports_px(config.width.max(1), config.height.max(1));
1918 log::debug!(
1919 "runmat-plot: renderer.scene_to_target.branch_subplot_axes viewports={}",
1920 viewports.len()
1921 );
1922 self.render_axes_to_viewports(
1923 encoder,
1924 target_view,
1925 &viewports,
1926 config.msaa_samples.max(1),
1927 config,
1928 )?;
1929 let stats = self.scene.statistics();
1930 Ok(RenderResult {
1931 success: true,
1932 data_bounds: self.data_bounds,
1933 axes_viewports_px: viewports,
1934 vertex_count: stats.total_vertices,
1935 triangle_count: stats.total_triangles,
1936 render_time_ms: start_time.elapsed().as_secs_f64() * 1000.0,
1937 })
1938 }
1939
1940 fn compute_tiled_viewports(
1941 total_width: u32,
1942 total_height: u32,
1943 rows: usize,
1944 cols: usize,
1945 ) -> Vec<(u32, u32, u32, u32)> {
1946 if rows == 0 || cols == 0 {
1947 return vec![(0, 0, total_width.max(1), total_height.max(1))];
1948 }
1949 let rows_u32 = rows as u32;
1950 let cols_u32 = cols as u32;
1951 let cell_w = (total_width / cols_u32).max(1);
1952 let cell_h = (total_height / rows_u32).max(1);
1953 let mut out = Vec::with_capacity(rows * cols);
1954 for r in 0..rows_u32 {
1955 for c in 0..cols_u32 {
1956 let x = c * cell_w;
1957 let y = r * cell_h;
1958 let mut w = cell_w;
1959 let mut h = cell_h;
1960 if c + 1 == cols_u32 {
1961 w = total_width.saturating_sub(x).max(1);
1962 }
1963 if r + 1 == rows_u32 {
1964 h = total_height.saturating_sub(y).max(1);
1965 }
1966 out.push((x, y, w, h));
1967 }
1968 }
1969 out
1970 }
1971
1972 pub fn compute_axes_viewports_px(
1973 &self,
1974 total_width: u32,
1975 total_height: u32,
1976 ) -> Vec<(u32, u32, u32, u32)> {
1977 let total_width = total_width.max(1);
1978 let total_height = total_height.max(1);
1979 let (rows, cols) = self.figure_axes_grid();
1980 let axes_count = self.figure_axes_count().max(1);
1981 let full = (0, 0, total_width, total_height);
1982 let mut viewports = if rows.saturating_mul(cols) > 1 {
1983 Self::compute_tiled_viewports(total_width, total_height, rows, cols)
1984 } else {
1985 vec![full; axes_count]
1986 };
1987 if viewports.len() < axes_count {
1988 let grid_len = viewports.len().max(1);
1989 for axes_index in viewports.len()..axes_count {
1990 let parent = self
1991 .overlay_parent_for_axes(axes_index)
1992 .unwrap_or(axes_index)
1993 .min(grid_len - 1);
1994 viewports.push(viewports.get(parent).copied().unwrap_or(full));
1995 }
1996 }
1997 viewports.truncate(axes_count);
1998 for (axes_index, viewport) in viewports.iter_mut().enumerate() {
1999 if let Some((position, units)) = self.overlay_position_for_axes(axes_index) {
2000 *viewport =
2001 Self::axes_position_to_viewport_px(total_width, total_height, position, &units);
2002 }
2003 }
2004 viewports
2005 }
2006
2007 fn axes_position_to_viewport_px(
2008 total_width: u32,
2009 total_height: u32,
2010 position: [f64; 4],
2011 units: &str,
2012 ) -> (u32, u32, u32, u32) {
2013 let tw = f64::from(total_width.max(1));
2014 let th = f64::from(total_height.max(1));
2015 let (left, bottom, width, height) = if units.eq_ignore_ascii_case("normalized") {
2016 (
2017 position[0] * tw,
2018 position[1] * th,
2019 position[2] * tw,
2020 position[3] * th,
2021 )
2022 } else {
2023 (position[0], position[1], position[2], position[3])
2024 };
2025 let x = left.clamp(0.0, tw - 1.0);
2026 let h = height.max(1.0).min(th);
2027 let y = (th - bottom - h).clamp(0.0, th - 1.0);
2028 let w = width.max(1.0).min(tw - x);
2029 let h = h.min(th - y);
2030 (
2031 x.round() as u32,
2032 y.round() as u32,
2033 w.round().max(1.0) as u32,
2034 h.round().max(1.0) as u32,
2035 )
2036 }
2037
2038 #[allow(clippy::too_many_arguments)]
2041 pub fn render_camera_to_viewport(
2042 &mut self,
2043 encoder: &mut wgpu::CommandEncoder,
2044 target_view: &wgpu::TextureView,
2045 viewport_scissor: (u32, u32, u32, u32),
2046 config: &PlotRenderConfig,
2047 camera: &Camera,
2048 axes_index: usize,
2049 clear_background: bool,
2050 ) -> Result<RenderResult, Box<dyn std::error::Error>> {
2051 log::debug!(
2052 "runmat-plot: renderer.camera_to_viewport.start axes_index={} viewport=({}, {}, {}, {}) clear_background={}",
2053 axes_index,
2054 viewport_scissor.0,
2055 viewport_scissor.1,
2056 viewport_scissor.2,
2057 viewport_scissor.3,
2058 clear_background
2059 );
2060 let use_msaa = config.msaa_samples.max(1) > 1;
2061 self.wgpu_renderer.ensure_msaa(config.msaa_samples);
2062 let msaa_view_keepalive = if use_msaa {
2063 Some(self.wgpu_renderer.ensure_msaa_color_view())
2064 } else {
2065 None
2066 };
2067 let render_target = if let Some(msaa_view) = msaa_view_keepalive.as_ref() {
2068 RenderTarget {
2069 view: msaa_view.as_ref(),
2070 resolve_target: Some(target_view),
2071 }
2072 } else {
2073 RenderTarget {
2074 view: target_view,
2075 resolve_target: None,
2076 }
2077 };
2078 self.render_camera_to_target_viewport(
2079 encoder,
2080 render_target,
2081 viewport_scissor,
2082 config,
2083 camera,
2084 axes_index,
2085 clear_background,
2086 )
2087 }
2088
2089 #[allow(clippy::too_many_arguments)]
2090 fn render_camera_to_target_viewport(
2091 &mut self,
2092 encoder: &mut wgpu::CommandEncoder,
2093 target: RenderTarget<'_>,
2094 viewport_scissor: (u32, u32, u32, u32),
2095 config: &PlotRenderConfig,
2096 camera: &Camera,
2097 axes_index: usize,
2098 clear_background: bool,
2099 ) -> Result<RenderResult, Box<dyn std::error::Error>> {
2100 let start_time = Instant::now();
2101
2102 self.wgpu_renderer.ensure_msaa(config.msaa_samples);
2104 self.wgpu_renderer.set_depth_mode(config.depth_mode);
2105
2106 let depth_view = self.wgpu_renderer.ensure_depth_view();
2110
2111 let aspect_ratio = (config.width.max(1)) as f32 / (config.height.max(1)) as f32;
2113 let mut cam = camera.clone();
2114 cam.update_aspect_ratio(aspect_ratio);
2115 cam.depth_mode = config.depth_mode;
2116 log::debug!(
2117 "runmat-plot: renderer.camera_to_target_viewport.camera_ready axes_index={} aspect_ratio={} msaa_samples={}",
2118 axes_index,
2119 aspect_ratio,
2120 config.msaa_samples
2121 );
2122
2123 if config.clip_policy.dynamic {
2126 let mut bounds: Option<crate::core::scene::BoundingBox> = None;
2127 for node in self.scene.get_visible_nodes() {
2128 if let Some(rd) = &node.render_data {
2129 if let Some(b) = rd.bounds {
2130 bounds = Some(bounds.map_or(b, |acc| acc.union(&b)));
2131 }
2132 }
2133 }
2134 if let Some(b) = bounds {
2135 cam.update_clip_planes_from_world_aabb(b.min, b.max, &config.clip_policy);
2136 }
2137 }
2138 let view_proj_matrix = cam.view_proj_matrix();
2139 let model_matrix = self.axes_model_matrix(axes_index);
2140 self.wgpu_renderer
2141 .update_uniforms_for_axes(axes_index, view_proj_matrix, model_matrix);
2142 log::debug!(
2143 "runmat-plot: renderer.camera_to_target_viewport.uniforms_updated axes_index={}",
2144 axes_index
2145 );
2146
2147 let (mut sx, mut sy, mut sw, mut sh) = viewport_scissor;
2148 let target_w = self.wgpu_renderer.surface_config.width.max(1);
2149 let target_h = self.wgpu_renderer.surface_config.height.max(1);
2150 if sx >= target_w || sy >= target_h {
2151 return Ok(RenderResult {
2152 success: true,
2153 data_bounds: self.data_bounds,
2154 axes_viewports_px: vec![(0, 0, target_w, target_h)],
2155 vertex_count: 0,
2156 triangle_count: 0,
2157 render_time_ms: 0.0,
2158 });
2159 }
2160 sx = sx.min(target_w.saturating_sub(1));
2161 sy = sy.min(target_h.saturating_sub(1));
2162 sw = sw.max(1).min(target_w.saturating_sub(sx).max(1));
2163 sh = sh.max(1).min(target_h.saturating_sub(sy).max(1));
2164 let is_2d = matches!(
2165 cam.projection,
2166 crate::core::camera::ProjectionType::Orthographic { .. }
2167 );
2168 log::debug!(
2169 "runmat-plot: renderer.camera_to_target_viewport.viewport_normalized axes_index={} viewport=({}, {}, {}, {}) is_2d={}",
2170 axes_index,
2171 sx,
2172 sy,
2173 sw,
2174 sh,
2175 is_2d
2176 );
2177 match cam.projection {
2178 crate::core::camera::ProjectionType::Orthographic {
2179 left,
2180 right,
2181 bottom,
2182 top,
2183 ..
2184 } => {
2185 log::debug!(
2186 target: "runmat_plot.draw_camera",
2187 "draw camera axes_index={} is_2d=true viewport=({}, {}, {}, {}) bounds=({}, {})..({}, {}) cfg_wh=({}, {})",
2188 axes_index,
2189 sx,
2190 sy,
2191 sw,
2192 sh,
2193 left,
2194 bottom,
2195 right,
2196 top,
2197 config.width,
2198 config.height
2199 );
2200 }
2201 crate::core::camera::ProjectionType::Perspective { .. } => {
2202 log::debug!(
2203 target: "runmat_plot.draw_camera",
2204 "draw camera axes_index={} is_2d=false viewport=({}, {}, {}, {}) cfg_wh=({}, {})",
2205 axes_index,
2206 sx,
2207 sy,
2208 sw,
2209 sh,
2210 config.width,
2211 config.height
2212 );
2213 }
2214 }
2215
2216 let mut owned_render_data: Vec<Box<crate::core::RenderData>> = Vec::new();
2218 let mut render_items = Vec::new();
2219 let mut grid_plane_buffers: Option<(wgpu::Buffer, wgpu::Buffer)> = None;
2220 let mut total_vertices = 0usize;
2221 let mut total_triangles = 0usize;
2222 let render_item_logs = render_item_diagnostics_enabled();
2223 log::debug!(
2224 "runmat-plot: renderer.camera_to_target_viewport.collect_render_items.start axes_index={}",
2225 axes_index
2226 );
2227 for node in self.scene.get_visible_nodes() {
2228 if let Some(render_data) = &node.render_data {
2229 if render_item_logs && node.axes_index == axes_index {
2230 log::debug!(
2231 target: "runmat_plot.draw_item",
2232 "draw item axes_index={} node_axes_index={} pipeline={:?} vertex_count={} has_indices={} has_bounds={} gpu_vertices={}",
2233 axes_index,
2234 node.axes_index,
2235 render_data.pipeline_type,
2236 render_data.vertex_count(),
2237 render_data.indices.is_some(),
2238 render_data.bounds.is_some(),
2239 render_data.gpu_vertices.is_some()
2240 );
2241 }
2242 if let Some((vb, ib)) = self.prepare_buffers_for_render_data(node.id, render_data) {
2243 self.wgpu_renderer
2244 .ensure_pipeline(render_data.pipeline_type);
2245 total_vertices += render_data.vertex_count();
2246 if let Some(indices) = &render_data.indices {
2247 total_triangles += indices.len() / 3;
2248 }
2249 render_items.push((render_data, vb, ib));
2250 }
2251 }
2252 }
2253 log::debug!(
2254 "runmat-plot: renderer.camera_to_target_viewport.collect_render_items.ok axes_index={} items={} total_vertices={} total_triangles={}",
2255 axes_index,
2256 render_items.len(),
2257 total_vertices,
2258 total_triangles
2259 );
2260
2261 if !is_2d && self.overlay_show_grid_for_axes(axes_index) {
2265 let view_proj = view_proj_matrix;
2266 let inv_view_proj = view_proj.inverse();
2267
2268 let unproject = |ndc_x: f32, ndc_y: f32, ndc_z: f32| -> Option<Vec3> {
2269 let clip = Vec4::new(ndc_x, ndc_y, ndc_z, 1.0);
2270 let world = inv_view_proj * clip;
2271 if !world.w.is_finite() || world.w.abs() < 1e-6 {
2272 return None;
2273 }
2274 let p = world.truncate() / world.w;
2275 if p.x.is_finite() && p.y.is_finite() && p.z.is_finite() {
2276 Some(p)
2277 } else {
2278 None
2279 }
2280 };
2281
2282 let ray_intersect_z0 = |ndc_x: f32, ndc_y: f32| -> Option<Vec3> {
2283 let p0 = unproject(ndc_x, ndc_y, -1.0)?;
2285 let p1 = unproject(ndc_x, ndc_y, 1.0)?;
2286 let dir = p1 - p0;
2287 if !dir.z.is_finite() || dir.z.abs() < 1e-8 {
2288 return None;
2289 }
2290 let t = (-p0.z) / dir.z;
2291 if !t.is_finite() || t <= 0.0 {
2292 return None;
2293 }
2294 Some(p0 + dir * t)
2295 };
2296
2297 let mut plane_pts: Vec<Vec3> = Vec::new();
2298 for (nx, ny) in [(-1.0, -1.0), (1.0, -1.0), (1.0, 1.0), (-1.0, 1.0)] {
2299 if let Some(p) = ray_intersect_z0(nx, ny) {
2300 plane_pts.push(p);
2301 }
2302 }
2303
2304 let mut min_x = 0.0_f32;
2306 let mut max_x = 1.0_f32;
2307 let mut min_y = 0.0_f32;
2308 let mut max_y = 1.0_f32;
2309
2310 if plane_pts.len() >= 2 {
2311 min_x = plane_pts.iter().map(|p| p.x).fold(f32::INFINITY, f32::min);
2312 max_x = plane_pts
2313 .iter()
2314 .map(|p| p.x)
2315 .fold(f32::NEG_INFINITY, f32::max);
2316 min_y = plane_pts.iter().map(|p| p.y).fold(f32::INFINITY, f32::min);
2317 max_y = plane_pts
2318 .iter()
2319 .map(|p| p.y)
2320 .fold(f32::NEG_INFINITY, f32::max);
2321 } else if let crate::core::camera::ProjectionType::Perspective { fov, .. } =
2322 cam.projection
2323 {
2324 let dist = (cam.position - cam.target).length().max(1e-3);
2325 let extent = (dist * (0.5 * fov).tan() * 1.25).max(0.5);
2326 let center = Vec3::new(cam.target.x, cam.target.y, 0.0);
2327 min_x = center.x - extent;
2328 max_x = center.x + extent;
2329 min_y = center.y - extent;
2330 max_y = center.y + extent;
2331 }
2332
2333 let dx = (max_x - min_x).abs().max(1e-3);
2335 let dy = (max_y - min_y).abs().max(1e-3);
2336 let margin_x = dx * 0.04;
2337 let margin_y = dy * 0.04;
2338 min_x -= margin_x;
2339 max_x += margin_x;
2340 min_y -= margin_y;
2341 max_y += margin_y;
2342
2343 let project_to_px = |p: Vec3| -> Option<(f32, f32)> {
2344 let clip = view_proj * Vec4::new(p.x, p.y, p.z, 1.0);
2345 if !clip.w.is_finite() || clip.w.abs() < 1e-6 {
2346 return None;
2347 }
2348 let ndc = clip.truncate() / clip.w;
2349 if !(ndc.x.is_finite() && ndc.y.is_finite()) {
2350 return None;
2351 }
2352 let px = ((ndc.x + 1.0) * 0.5) * (sw.max(1) as f32);
2353 let py = ((1.0 - ndc.y) * 0.5) * (sh.max(1) as f32);
2354 Some((px, py))
2355 };
2356
2357 let nice_step = |raw: f64| -> f64 {
2358 if !raw.is_finite() || raw <= 0.0 {
2359 return 1.0;
2360 }
2361 let pow10 = 10.0_f64.powf(raw.log10().floor());
2362 let norm = raw / pow10;
2363 let mult = if norm <= 1.0 {
2364 1.0
2365 } else if norm <= 2.0 {
2366 2.0
2367 } else if norm <= 5.0 {
2368 5.0
2369 } else {
2370 10.0
2371 };
2372 mult * pow10
2373 };
2374
2375 let cx = (min_x + max_x) * 0.5;
2377 let cy = (min_y + max_y) * 0.5;
2378 let center = Vec3::new(cx, cy, 0.0);
2379 let px_per_world = {
2380 let a = project_to_px(center);
2381 let b = project_to_px(center + Vec3::new(1.0, 0.0, 0.0));
2382 match (a, b) {
2383 (Some((ax, ay)), Some((bx, by))) => ((bx - ax).hypot(by - ay)).max(1e-3),
2384 _ => 1.0,
2385 }
2386 };
2387 let desired_major_px = 120.0_f64;
2388 let major_step = nice_step((desired_major_px / (px_per_world as f64)).max(1e-6));
2389 let mut minor_step = major_step / 10.0;
2390 if !minor_step.is_finite() || minor_step <= 0.0 {
2391 minor_step = major_step.max(1.0);
2392 }
2393
2394 let max_minor_lines = 180.0;
2396 let minor_count_x = (dx as f64 / minor_step).abs();
2397 let minor_count_y = (dy as f64 / minor_step).abs();
2398 if minor_count_x > max_minor_lines || minor_count_y > max_minor_lines {
2399 minor_step = (major_step / 5.0).max(major_step); }
2401
2402 let mut helper_vertices: Vec<Vertex> = Vec::new();
2403 let mut push_line = |a: Vec3, b: Vec3, color: Vec4| {
2404 helper_vertices.push(Vertex::new(a, color));
2405 helper_vertices.push(Vertex::new(b, color));
2406 };
2407
2408 let z_grid = -1e-4_f32;
2410
2411 let show_major_grid = self.overlay_show_grid_for_axes(axes_index);
2414 let show_minor_grid = self.overlay_show_minor_grid_for_axes(axes_index);
2415 if show_major_grid || show_minor_grid {
2416 let theme = self.theme.build_theme();
2417 let bg = theme.get_background_color();
2418 let grid = theme.get_grid_color();
2419 let bg_luma = 0.2126 * bg.x + 0.7152 * bg.y + 0.0722 * bg.z;
2420 let mut major_rgb = [grid.x, grid.y, grid.z];
2421 let mut minor_rgb = [grid.x, grid.y, grid.z];
2422 let mut major_alpha = grid.w.clamp(0.08, 0.22);
2423 let mut minor_alpha = (grid.w * 0.45).clamp(0.04, 0.14);
2424 if bg_luma <= 0.62 {
2425 major_rgb = [grid.x * 0.80, grid.y * 0.80, grid.z * 0.80];
2426 minor_rgb = [grid.x * 0.68, grid.y * 0.68, grid.z * 0.68];
2427 }
2428 if bg_luma > 0.62 {
2429 major_rgb = [grid.x * 0.45, grid.y * 0.45, grid.z * 0.45];
2430 minor_rgb = [grid.x * 0.33, grid.y * 0.33, grid.z * 0.33];
2431 major_alpha = major_alpha.max(0.24);
2432 minor_alpha = minor_alpha.max(0.12);
2433 }
2434 if !show_major_grid {
2435 major_alpha = 0.0;
2436 }
2437 if !show_minor_grid {
2438 minor_alpha = 0.0;
2439 }
2440 self.wgpu_renderer.ensure_grid_plane_pipeline();
2441 self.wgpu_renderer.update_grid_uniforms_for_axes(
2442 axes_index,
2443 crate::core::renderer::GridUniforms {
2444 major_step: major_step as f32,
2445 minor_step: minor_step as f32,
2446 fade_start: (0.60 * dx.max(dy)).max(major_step as f32),
2447 fade_end: (0.95 * dx.max(dy)).max((major_step as f32) * 2.0),
2448 camera_pos: cam.position.to_array(),
2449 _pad0: 0.0,
2450 target_pos: Vec3::new(cam.target.x, cam.target.y, 0.0).to_array(),
2451 _pad1: 0.0,
2452 major_color: [major_rgb[0], major_rgb[1], major_rgb[2], major_alpha],
2453 minor_color: [minor_rgb[0], minor_rgb[1], minor_rgb[2], minor_alpha],
2454 },
2455 );
2456
2457 let quad_vertices = [
2458 Vertex::new(Vec3::new(min_x, min_y, z_grid), Vec4::ONE),
2459 Vertex::new(Vec3::new(max_x, min_y, z_grid), Vec4::ONE),
2460 Vertex::new(Vec3::new(max_x, max_y, z_grid), Vec4::ONE),
2461 Vertex::new(Vec3::new(min_x, max_y, z_grid), Vec4::ONE),
2462 ];
2463 let quad_indices: [u32; 6] = [0, 1, 2, 0, 2, 3];
2464 let vb = self.wgpu_renderer.create_vertex_buffer(&quad_vertices);
2465 let ib = self.wgpu_renderer.create_index_buffer(&quad_indices);
2466 grid_plane_buffers = Some((vb, ib));
2467 }
2468
2469 let axis_len = (major_step as f32 * 5.0).clamp(0.5, (dx.max(dy) * 0.6).max(0.5));
2471 let origin = Vec3::new(0.0, 0.0, 0.0);
2472 let col_x = Vec4::new(0.92, 0.25, 0.25, 0.85);
2473 let col_y = Vec4::new(0.35, 0.90, 0.45, 0.85);
2474 let col_z = Vec4::new(0.35, 0.62, 0.98, 0.85);
2475 push_line(origin, origin + Vec3::new(axis_len, 0.0, 0.0), col_x);
2476 push_line(origin, origin + Vec3::new(0.0, axis_len, 0.0), col_y);
2477 push_line(origin, origin + Vec3::new(0.0, 0.0, axis_len), col_z);
2478
2479 let tick_max = (major_step as f32 * 0.25).max(1.0e-6);
2485 let tick_min = 0.01_f32.min(tick_max);
2486 let tick_len = (axis_len * 0.04).clamp(tick_min, tick_max);
2487 let max_ticks = 6usize;
2488 let mut add_ticks = |axis: Vec3, perp: Vec3, col: Vec4| {
2489 if major_step <= 0.0 {
2490 return;
2491 }
2492 for i in 1..=max_ticks {
2493 let t = (i as f32) * (major_step as f32);
2494 if t >= axis_len * 0.999 {
2495 break;
2496 }
2497 let p = origin + axis * t;
2498 push_line(
2499 p - perp * tick_len,
2500 p + perp * tick_len,
2501 Vec4::new(col.x, col.y, col.z, col.w * 0.85),
2502 );
2503 }
2504 };
2505 add_ticks(Vec3::X, Vec3::Y, col_x);
2506 add_ticks(Vec3::Y, Vec3::X, col_y);
2507 add_ticks(Vec3::Z, Vec3::X, col_z);
2508
2509 if !helper_vertices.is_empty() {
2510 let rd = Box::new(crate::core::RenderData {
2511 pipeline_type: crate::core::PipelineType::Lines,
2512 vertices: helper_vertices,
2513 indices: None,
2514 gpu_vertices: None,
2515 bounds: None,
2516 material: crate::core::Material::default(),
2517 draw_calls: vec![crate::core::DrawCall {
2518 vertex_offset: 0,
2519 vertex_count: 0, index_offset: None,
2521 index_count: None,
2522 instance_count: 1,
2523 }],
2524 image: None,
2525 });
2526 owned_render_data.push(rd);
2527 let idx = owned_render_data.len() - 1;
2528 let vcount = owned_render_data[idx].vertices.len();
2530 if let Some(dc) = owned_render_data[idx].draw_calls.get_mut(0) {
2531 dc.vertex_count = vcount;
2532 }
2533 let vb = Arc::new(
2534 self.wgpu_renderer
2535 .create_vertex_buffer(&owned_render_data[idx].vertices),
2536 );
2537 let rd_ref: &crate::core::RenderData = &owned_render_data[idx];
2539 render_items.insert(0, (rd_ref, vb, None));
2540 total_vertices += vcount;
2541 }
2542 }
2543
2544 let mut point_buffers: Vec<Option<(wgpu::Buffer, usize)>> =
2546 Vec::with_capacity(render_items.len());
2547 for (render_data, _vb, _ib) in render_items.iter() {
2548 if matches!(
2549 render_data.pipeline_type,
2550 crate::core::PipelineType::Points | crate::core::PipelineType::Scatter3
2551 ) && !render_data.vertices.is_empty()
2552 {
2553 let expanded = self
2554 .wgpu_renderer
2555 .create_direct_point_vertices(&render_data.vertices, 0.0);
2557 let buf = self.wgpu_renderer.create_vertex_buffer(&expanded);
2558 point_buffers.push(Some((buf, expanded.len())));
2559 } else {
2560 point_buffers.push(None);
2561 }
2562 }
2563 let has_textured_items = render_items.iter().any(|(render_data, _vb, _ib)| {
2565 render_data.pipeline_type == crate::core::PipelineType::Textured
2566 });
2567 if has_textured_items {
2568 self.wgpu_renderer.ensure_image_pipeline();
2569 }
2570 let mut image_bind_groups: Vec<Option<wgpu::BindGroup>> =
2571 Vec::with_capacity(render_items.len());
2572
2573 for (render_data, _vb, _ib) in render_items.iter() {
2574 if render_data.pipeline_type == crate::core::PipelineType::Textured {
2575 if let Some(crate::core::scene::ImageData::Rgba8 {
2576 width,
2577 height,
2578 data,
2579 }) = &render_data.image
2580 {
2581 let (_t, _v, bg) = self
2582 .wgpu_renderer
2583 .create_image_texture_and_bind_group(*width, *height, data);
2584 image_bind_groups.push(Some(bg));
2585 } else {
2586 image_bind_groups.push(None);
2587 }
2588 } else {
2589 image_bind_groups.push(None);
2590 }
2591 }
2592 let mut point_style_bind_groups: Vec<Option<wgpu::BindGroup>> =
2594 Vec::with_capacity(render_items.len());
2595 for (render_data, _vb, _ib) in render_items.iter() {
2596 if matches!(
2597 render_data.pipeline_type,
2598 crate::core::PipelineType::Points | crate::core::PipelineType::Scatter3
2599 ) {
2600 let style = crate::core::renderer::PointStyleUniforms {
2601 face_color: render_data.material.albedo.to_array(),
2602 edge_color: render_data.material.emissive.to_array(),
2603 edge_thickness_px: render_data.material.roughness,
2604 marker_shape: render_data.material.metallic as u32,
2605 _pad: [0.0, 0.0],
2606 };
2607 let (_buf, bg) = self.wgpu_renderer.create_point_style_bind_group(style);
2608 point_style_bind_groups.push(Some(bg));
2609 } else {
2610 point_style_bind_groups.push(None);
2611 }
2612 }
2613
2614 let mut grid_vb_opt: Option<wgpu::Buffer> = None;
2617 let show_major_grid = self.overlay_show_grid_for_axes(axes_index);
2618 let show_minor_grid = self.overlay_show_minor_grid_for_axes(axes_index);
2619 let log_flags = self.direct_log_flags_for_axes(axes_index);
2620 if is_2d && log_flags == [0, 0] && (show_major_grid || show_minor_grid) {
2621 if let Some((mut l, mut r, mut b, mut t)) = self.view_bounds_for_axes(axes_index) {
2622 if self.overlay_axes_kind_for_axes(axes_index) == AxesKind::Polar {
2623 let radius = l.abs().max(r.abs()).max(b.abs()).max(t.abs()).max(1e-6);
2624 l = -radius;
2625 r = radius;
2626 b = -radius;
2627 t = radius;
2628 }
2629 self.wgpu_renderer.update_direct_uniforms_for_axes(
2631 axes_index,
2632 DirectUniforms::new(
2633 [l as f32, b as f32],
2634 [r as f32, t as f32],
2635 [-1.0, -1.0],
2636 [1.0, 1.0],
2637 [sw.max(1) as f32, sh.max(1) as f32],
2638 log_flags,
2639 ),
2640 );
2641 self.wgpu_renderer.ensure_direct_line_pipeline();
2642
2643 let x_range = (r - l).max(1e-6);
2644 let y_range = (t - b).max(1e-6);
2645 let x_step = plot_utils::calculate_tick_interval(x_range);
2646 let y_step = plot_utils::calculate_tick_interval(y_range);
2647 let mut grid_vertices: Vec<Vertex> = Vec::new();
2648 let g = 80.0_f32 / 255.0_f32;
2649 let major_col = Vec4::new(g, g, g, 1.0);
2650 let minor_col = Vec4::new(g, g, g, 0.42);
2651 if self.overlay_axes_kind_for_axes(axes_index) == AxesKind::Polar {
2652 let radius = l.abs().max(r.abs()).max(b.abs()).max(t.abs()).max(1e-6);
2653 let step = plot_utils::calculate_tick_interval(radius);
2654 Self::push_polar_grid_lines(
2655 &mut grid_vertices,
2656 radius,
2657 step,
2658 show_major_grid,
2659 show_minor_grid,
2660 major_col,
2661 minor_col,
2662 );
2663 } else {
2664 if show_minor_grid && x_step.is_finite() && x_step > 0.0 {
2665 let minor_step = (x_step / 5.0).max(f64::EPSILON);
2666 Self::push_vertical_grid_lines(
2667 &mut grid_vertices,
2668 (l, r),
2669 (b, t),
2670 minor_step,
2671 minor_col,
2672 Some((x_step, minor_step * 0.25)),
2673 );
2674 }
2675 if show_minor_grid && y_step.is_finite() && y_step > 0.0 {
2676 let minor_step = (y_step / 5.0).max(f64::EPSILON);
2677 Self::push_horizontal_grid_lines(
2678 &mut grid_vertices,
2679 (b, t),
2680 (l, r),
2681 minor_step,
2682 minor_col,
2683 Some((y_step, minor_step * 0.25)),
2684 );
2685 }
2686 if show_major_grid && x_step.is_finite() && x_step > 0.0 {
2687 Self::push_vertical_grid_lines(
2688 &mut grid_vertices,
2689 (l, r),
2690 (b, t),
2691 x_step,
2692 major_col,
2693 None,
2694 );
2695 }
2696 if show_major_grid && y_step.is_finite() && y_step > 0.0 {
2697 Self::push_horizontal_grid_lines(
2698 &mut grid_vertices,
2699 (b, t),
2700 (l, r),
2701 y_step,
2702 major_col,
2703 None,
2704 );
2705 }
2706 }
2707 if !grid_vertices.is_empty() {
2708 grid_vb_opt = Some(self.wgpu_renderer.create_vertex_buffer(&grid_vertices));
2709 }
2710 }
2711 }
2712
2713 let bounds_opt = if is_2d {
2715 match cam.projection {
2716 crate::core::camera::ProjectionType::Orthographic {
2717 left,
2718 right,
2719 bottom,
2720 top,
2721 ..
2722 } => Some((left as f64, right as f64, bottom as f64, top as f64)),
2723 _ => self.data_bounds,
2724 }
2725 } else {
2726 None
2727 };
2728 if is_2d {
2729 if let Some((l, r, b, t)) = bounds_opt {
2730 self.wgpu_renderer.update_direct_uniforms_for_axes(
2731 axes_index,
2732 DirectUniforms::new(
2733 [l as f32, b as f32],
2734 [r as f32, t as f32],
2735 [-1.0, -1.0],
2736 [1.0, 1.0],
2737 [sw.max(1) as f32, sh.max(1) as f32],
2738 log_flags,
2739 ),
2740 );
2741 }
2742 self.wgpu_renderer.ensure_direct_triangle_pipeline();
2743 self.wgpu_renderer.ensure_direct_line_pipeline();
2744 self.wgpu_renderer.ensure_direct_point_pipeline();
2745 } else {
2746 self.wgpu_renderer
2748 .ensure_pipeline(crate::core::PipelineType::Triangles);
2749 self.wgpu_renderer
2750 .ensure_pipeline(crate::core::PipelineType::Lines);
2751 self.wgpu_renderer
2752 .ensure_pipeline(crate::core::PipelineType::LinesNoDepth);
2753 self.wgpu_renderer
2754 .ensure_pipeline(crate::core::PipelineType::Points);
2755 }
2756 self.wgpu_renderer.update_marker_screen_uniforms_for_axes(
2757 axes_index,
2758 [sw.max(1) as f32, sh.max(1) as f32],
2759 );
2760
2761 {
2763 let use_msaa = self.wgpu_renderer.msaa_sample_count > 1;
2765 log::debug!(
2766 "runmat-plot: renderer.camera_to_target_viewport.render_pass_start axes_index={} use_msaa={} clear_background={}",
2767 axes_index,
2768 use_msaa,
2769 clear_background
2770 );
2771
2772 let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
2773 label: Some("Plot Camera Viewport Pass"),
2774 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
2775 view: target.view,
2776 resolve_target: if use_msaa {
2777 target.resolve_target
2778 } else {
2779 None
2780 },
2781 ops: wgpu::Operations {
2782 load: if clear_background {
2783 wgpu::LoadOp::Clear(wgpu::Color {
2784 r: config.background_color.x as f64,
2785 g: config.background_color.y as f64,
2786 b: config.background_color.z as f64,
2787 a: config.background_color.w as f64,
2788 })
2789 } else {
2790 wgpu::LoadOp::Load
2791 },
2792 store: wgpu::StoreOp::Store,
2793 },
2794 })],
2795 depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
2796 view: depth_view.as_ref(),
2797 depth_ops: Some(wgpu::Operations {
2798 load: wgpu::LoadOp::Clear(match config.depth_mode {
2799 DepthMode::Standard => 1.0,
2800 DepthMode::ReversedZ => 0.0,
2801 }),
2802 store: wgpu::StoreOp::Store,
2803 }),
2804 stencil_ops: None,
2805 }),
2806 timestamp_writes: None,
2807 occlusion_query_set: None,
2808 });
2809
2810 render_pass.set_viewport(
2812 sx as f32,
2813 sy as f32,
2814 sw.max(1) as f32,
2815 sh.max(1) as f32,
2816 0.0,
2817 1.0,
2818 );
2819 render_pass.set_scissor_rect(sx, sy, sw.max(1), sh.max(1));
2820 log::debug!(
2821 "runmat-plot: renderer.camera_to_target_viewport.render_pass_ready axes_index={} viewport=({}, {}, {}, {})",
2822 axes_index,
2823 sx,
2824 sy,
2825 sw.max(1),
2826 sh.max(1)
2827 );
2828 if let Some(ref vb_grid) = grid_vb_opt {
2829 if let Some(ref pipeline) = self.wgpu_renderer.direct_line_pipeline {
2830 log::debug!(
2831 "runmat-plot: renderer.camera_to_target_viewport.draw_grid_start axes_index={} vertex_buffer_size={}",
2832 axes_index,
2833 vb_grid.size()
2834 );
2835 render_pass.set_pipeline(pipeline);
2836 render_pass.set_bind_group(
2837 0,
2838 self.wgpu_renderer
2839 .get_direct_uniform_bind_group_for_axes(axes_index),
2840 &[],
2841 );
2842 render_pass.set_vertex_buffer(0, vb_grid.slice(..));
2843 render_pass.draw(
2848 0..(vb_grid.size() / std::mem::size_of::<Vertex>() as u64) as u32,
2849 0..1,
2850 );
2851 log::debug!(
2852 "runmat-plot: renderer.camera_to_target_viewport.draw_grid_ok axes_index={}",
2853 axes_index
2854 );
2855 }
2856 }
2857
2858 let use_direct_for_triangles = is_2d;
2860 let use_direct_for_lines = is_2d;
2861 let direct_tri_pipeline = if use_direct_for_triangles && bounds_opt.is_some() {
2862 self.wgpu_renderer
2863 .direct_triangle_pipeline
2864 .as_ref()
2865 .map(|p| p as *const wgpu::RenderPipeline)
2866 } else {
2867 None
2868 };
2869 let direct_line_pipeline = if use_direct_for_lines && bounds_opt.is_some() {
2870 self.wgpu_renderer
2871 .direct_line_pipeline
2872 .as_ref()
2873 .map(|p| p as *const wgpu::RenderPipeline)
2874 } else {
2875 None
2876 };
2877 let direct_point_pipeline = if is_2d && bounds_opt.is_some() {
2878 self.wgpu_renderer
2879 .direct_point_pipeline
2880 .as_ref()
2881 .map(|p| p as *const wgpu::RenderPipeline)
2882 } else {
2883 None
2884 };
2885
2886 for (idx, (render_data, vertex_buffer, index_buffer)) in render_items.iter().enumerate()
2887 {
2888 let is_triangles = matches!(
2889 render_data.pipeline_type,
2890 crate::core::PipelineType::Triangles
2891 );
2892 let is_lines = matches!(
2893 render_data.pipeline_type,
2894 crate::core::PipelineType::Lines | crate::core::PipelineType::LinesNoDepth
2895 );
2896 let is_points = matches!(
2897 render_data.pipeline_type,
2898 crate::core::PipelineType::Points | crate::core::PipelineType::Scatter3
2899 );
2900 let is_textured = matches!(
2901 render_data.pipeline_type,
2902 crate::core::PipelineType::Textured
2903 );
2904 let use_direct = is_2d
2906 && ((use_direct_for_triangles && is_triangles)
2907 || (use_direct_for_lines && is_lines)
2908 || is_points)
2909 && bounds_opt.is_some();
2910 if render_item_logs {
2911 log::debug!(
2912 "runmat-plot: renderer.camera_to_target_viewport.draw_item_start axes_index={} item_index={} pipeline={:?} use_direct={} textured={} indexed={} draw_calls={} point_buffer={} ",
2913 axes_index,
2914 idx,
2915 render_data.pipeline_type,
2916 use_direct,
2917 is_textured,
2918 index_buffer.is_some(),
2919 render_data.draw_calls.len(),
2920 point_buffers[idx].is_some()
2921 );
2922 }
2923
2924 if use_direct {
2925 let pipeline_ref: &wgpu::RenderPipeline = unsafe {
2927 if is_triangles {
2928 direct_tri_pipeline.unwrap().as_ref().unwrap()
2929 } else if is_lines {
2930 direct_line_pipeline.unwrap().as_ref().unwrap()
2931 } else {
2932 direct_point_pipeline.unwrap().as_ref().unwrap()
2933 }
2934 };
2935 let uniform_bg = self
2936 .wgpu_renderer
2937 .get_direct_uniform_bind_group_for_axes(axes_index);
2938 render_pass.set_pipeline(pipeline_ref);
2939 render_pass.set_bind_group(0, uniform_bg, &[]);
2940 if is_points {
2941 if let Some(ref bg) = point_style_bind_groups[idx] {
2942 render_pass.set_bind_group(1, bg, &[]);
2943 }
2944 }
2945 if render_item_logs {
2946 log::debug!(
2947 "runmat-plot: renderer.camera_to_target_viewport.draw_item_pipeline_ready axes_index={} item_index={} branch=direct",
2948 axes_index,
2949 idx
2950 );
2951 }
2952 } else if is_textured {
2953 let pipeline = self
2954 .wgpu_renderer
2955 .get_pipeline(crate::core::PipelineType::Textured);
2956 render_pass.set_pipeline(pipeline);
2957 render_pass.set_bind_group(
2958 0,
2959 self.wgpu_renderer
2960 .get_direct_uniform_bind_group_for_axes(axes_index),
2961 &[],
2962 );
2963 if let Some(ref bg) = image_bind_groups[idx] {
2964 render_pass.set_bind_group(1, bg, &[]);
2965 }
2966 if render_item_logs {
2967 log::debug!(
2968 "runmat-plot: renderer.camera_to_target_viewport.draw_item_pipeline_ready axes_index={} item_index={} branch=textured",
2969 axes_index,
2970 idx
2971 );
2972 }
2973 } else {
2974 let pipeline = self.wgpu_renderer.get_pipeline(render_data.pipeline_type);
2975 render_pass.set_pipeline(pipeline);
2976 render_pass.set_bind_group(
2977 0,
2978 self.wgpu_renderer
2979 .get_uniform_bind_group_for_axes(axes_index),
2980 &[],
2981 );
2982 if is_points {
2983 if let Some(ref bg) = point_style_bind_groups[idx] {
2984 render_pass.set_bind_group(1, bg, &[]);
2985 }
2986 render_pass.set_bind_group(
2987 2,
2988 self.wgpu_renderer
2989 .get_marker_screen_bind_group_for_axes(axes_index),
2990 &[],
2991 );
2992 }
2993 if render_item_logs {
2994 log::debug!(
2995 "runmat-plot: renderer.camera_to_target_viewport.draw_item_pipeline_ready axes_index={} item_index={} branch=standard",
2996 axes_index,
2997 idx
2998 );
2999 }
3000 }
3001
3002 if is_points && use_direct {
3003 if let Some((ref buf, len)) = point_buffers[idx] {
3004 render_pass.set_vertex_buffer(0, buf.slice(..));
3005 render_pass.draw(0..len as u32, 0..1);
3006 if render_item_logs {
3007 log::debug!(
3008 "runmat-plot: renderer.camera_to_target_viewport.draw_item_ok axes_index={} item_index={} mode=direct_points vertices={}",
3009 axes_index,
3010 idx,
3011 len
3012 );
3013 }
3014 continue;
3015 }
3016 } else if is_points {
3017 if let Some((ref buf, _len)) = point_buffers[idx] {
3018 render_pass.set_vertex_buffer(0, buf.slice(..));
3019 } else {
3020 render_pass.set_vertex_buffer(0, vertex_buffer.slice(..));
3021 }
3022 } else {
3023 render_pass.set_vertex_buffer(0, vertex_buffer.slice(..));
3024 }
3025 if let Some(index_buffer_ref) = index_buffer {
3026 render_pass
3027 .set_index_buffer(index_buffer_ref.slice(..), wgpu::IndexFormat::Uint32);
3028 if let Some(indices) = &render_data.indices {
3029 render_pass.draw_indexed(0..indices.len() as u32, 0, 0..1);
3030 if render_item_logs {
3031 log::debug!(
3032 "runmat-plot: renderer.camera_to_target_viewport.draw_item_ok axes_index={} item_index={} mode=indexed indices={}",
3033 axes_index,
3034 idx,
3035 indices.len()
3036 );
3037 }
3038 }
3039 } else {
3040 if is_points {
3041 if let Some((_, len)) = point_buffers[idx] {
3042 render_pass.draw(0..len as u32, 0..1);
3043 continue;
3044 }
3045 }
3046 for dc in &render_data.draw_calls {
3047 render_pass.draw(
3048 dc.vertex_offset as u32..(dc.vertex_offset + dc.vertex_count) as u32,
3049 0..dc.instance_count as u32,
3050 );
3051 if render_item_logs {
3052 log::debug!(
3053 "runmat-plot: renderer.camera_to_target_viewport.draw_call_ok axes_index={} item_index={} mode=draw vertex_offset={} vertex_count={} instances={}",
3054 axes_index,
3055 idx,
3056 dc.vertex_offset,
3057 dc.vertex_count,
3058 dc.instance_count
3059 );
3060 }
3061 }
3062 }
3063 }
3064
3065 if let Some((ref vb, ref ib)) = grid_plane_buffers {
3067 if let Some(pipeline) = self.wgpu_renderer.grid_plane_pipeline() {
3068 log::debug!(
3069 "runmat-plot: renderer.camera_to_target_viewport.draw_grid_plane_start axes_index={}",
3070 axes_index
3071 );
3072 render_pass.set_pipeline(pipeline);
3073 render_pass.set_bind_group(
3074 0,
3075 self.wgpu_renderer
3076 .get_uniform_bind_group_for_axes(axes_index),
3077 &[],
3078 );
3079 render_pass.set_bind_group(
3080 1,
3081 self.wgpu_renderer
3082 .get_grid_uniform_bind_group_for_axes(axes_index),
3083 &[],
3084 );
3085 render_pass.set_vertex_buffer(0, vb.slice(..));
3086 render_pass.set_index_buffer(ib.slice(..), wgpu::IndexFormat::Uint32);
3087 render_pass.draw_indexed(0..6, 0, 0..1);
3088 log::debug!(
3089 "runmat-plot: renderer.camera_to_target_viewport.draw_grid_plane_ok axes_index={}",
3090 axes_index
3091 );
3092 }
3093 }
3094 }
3095
3096 log::debug!(
3097 "runmat-plot: renderer.camera_to_target_viewport.ok axes_index={} total_vertices={} total_triangles={}",
3098 axes_index,
3099 total_vertices,
3100 total_triangles
3101 );
3102
3103 Ok(RenderResult {
3104 success: true,
3105 data_bounds: self.data_bounds,
3106 axes_viewports_px: vec![(sx, sy, sw, sh)],
3107 vertex_count: total_vertices,
3108 triangle_count: total_triangles,
3109 render_time_ms: start_time.elapsed().as_secs_f64() * 1000.0,
3110 })
3111 }
3112
3113 pub fn render_axes_to_viewports(
3116 &mut self,
3117 encoder: &mut wgpu::CommandEncoder,
3118 target_view: &wgpu::TextureView,
3119 axes_viewports: &[(u32, u32, u32, u32)],
3120 msaa_samples: u32,
3121 base_config: &PlotRenderConfig,
3122 ) -> Result<(), Box<dyn std::error::Error>> {
3123 log::debug!(
3124 "runmat-plot: renderer.axes_to_viewports.start viewport_count={} msaa_samples={} width={} height={}",
3125 axes_viewports.len(),
3126 msaa_samples,
3127 base_config.width,
3128 base_config.height
3129 );
3130 let mut axes_to_nodes: std::collections::HashMap<usize, Vec<crate::core::scene::NodeId>> =
3132 std::collections::HashMap::new();
3133 for node in self.scene.get_visible_nodes() {
3134 axes_to_nodes
3135 .entry(node.axes_index)
3136 .or_default()
3137 .push(node.id);
3138 }
3139
3140 if self.axes_cameras.is_empty() {
3141 self.axes_cameras.push(Self::create_default_camera());
3142 }
3143 self.wgpu_renderer
3144 .ensure_axes_uniform_capacity(axes_viewports.len().max(1));
3145
3146 let all_ids: Vec<crate::core::scene::NodeId> = self
3148 .scene
3149 .get_visible_nodes()
3150 .into_iter()
3151 .map(|n| n.id)
3152 .collect();
3153 let active_axes: Vec<usize> = axes_viewports
3154 .iter()
3155 .enumerate()
3156 .filter_map(|(ax_idx, _)| {
3157 axes_to_nodes
3158 .get(&ax_idx)
3159 .filter(|ids| !ids.is_empty())
3160 .map(|_| ax_idx)
3161 })
3162 .collect();
3163 if active_axes.is_empty() {
3164 log::debug!("runmat-plot: renderer.axes_to_viewports.no_active_axes");
3165 return Ok(());
3166 }
3167
3168 self.wgpu_renderer.ensure_msaa(msaa_samples.max(1));
3169 let shared_msaa_view = if self.wgpu_renderer.msaa_sample_count > 1 {
3170 Some(self.wgpu_renderer.ensure_msaa_color_view())
3171 } else {
3172 None
3173 };
3174
3175 for (ax_idx, viewport) in axes_viewports.iter().enumerate() {
3176 log::debug!(
3177 "runmat-plot: renderer.axes_to_viewports.viewport axes_index={} viewport=({}, {}, {}, {})",
3178 ax_idx,
3179 viewport.0,
3180 viewport.1,
3181 viewport.2,
3182 viewport.3
3183 );
3184 let ids_for_axes = axes_to_nodes.get(&ax_idx).cloned().unwrap_or_default();
3185 if ids_for_axes.is_empty() {
3186 log::debug!(
3187 "runmat-plot: renderer.axes_to_viewports.skip_empty_axes axes_index={}",
3188 ax_idx
3189 );
3190 continue;
3191 }
3192
3193 let mut hidden_ids: Vec<crate::core::scene::NodeId> = Vec::new();
3195 for id in &all_ids {
3196 if !ids_for_axes.contains(id) {
3197 if let Some(node) = self.scene.get_node_mut(*id) {
3198 if node.visible {
3199 node.visible = false;
3200 hidden_ids.push(*id);
3201 }
3202 }
3203 }
3204 }
3205 let cam = self
3207 .axes_cameras
3208 .get(ax_idx)
3209 .cloned()
3210 .unwrap_or_else(Self::create_default_camera);
3211 let _ = self.calculate_data_bounds();
3212
3213 let mut cfg = base_config.clone();
3215 cfg.width = viewport.2;
3216 cfg.height = viewport.3;
3217 cfg.msaa_samples = msaa_samples.max(1);
3218 let is_first_axes = Some(&ax_idx) == active_axes.first();
3219 let is_last_axes = Some(&ax_idx) == active_axes.last();
3220 log::debug!(
3221 "runmat-plot: renderer.axes_to_viewports.axes_ready axes_index={} node_count={} first_axes={} last_axes={}",
3222 ax_idx,
3223 ids_for_axes.len(),
3224 is_first_axes,
3225 is_last_axes
3226 );
3227 let render_target = if let Some(ref msaa_view) = shared_msaa_view {
3228 RenderTarget {
3229 view: msaa_view.as_ref(),
3230 resolve_target: if is_last_axes {
3231 Some(target_view)
3232 } else {
3233 None
3234 },
3235 }
3236 } else {
3237 RenderTarget {
3238 view: target_view,
3239 resolve_target: None,
3240 }
3241 };
3242 let _ = self.render_camera_to_target_viewport(
3243 encoder,
3244 render_target,
3245 *viewport,
3246 &cfg,
3247 &cam,
3248 ax_idx,
3249 is_first_axes,
3250 )?;
3251 log::debug!(
3252 "runmat-plot: renderer.axes_to_viewports.axes_render_ok axes_index={}",
3253 ax_idx
3254 );
3255
3256 for id in hidden_ids {
3258 if let Some(node) = self.scene.get_node_mut(id) {
3259 node.visible = true;
3260 }
3261 }
3262 }
3263 log::debug!("runmat-plot: renderer.axes_to_viewports.ok");
3264 Ok(())
3265 }
3266
3267 fn create_default_camera() -> Camera {
3269 let mut camera = Camera::new();
3270 camera.projection = crate::core::camera::ProjectionType::Orthographic {
3271 left: -5.0,
3272 right: 5.0,
3273 bottom: -5.0,
3274 top: 5.0,
3275 near: -10.0,
3277 far: 10.0,
3278 };
3279 camera.depth_mode = DepthMode::default();
3280 camera.position = Vec3::new(0.0, 0.0, 1.0);
3282 camera.target = Vec3::new(0.0, 0.0, 0.0);
3283 camera.up = Vec3::new(0.0, 1.0, 0.0);
3284 camera
3285 }
3286
3287 pub fn camera(&self) -> &Camera {
3291 self.axes_cameras
3292 .first()
3293 .expect("axes_cameras must contain at least one camera")
3294 }
3295
3296 pub fn camera_mut(&mut self) -> &mut Camera {
3298 self.axes_cameras
3299 .first_mut()
3300 .expect("axes_cameras must contain at least one camera")
3301 }
3302
3303 pub fn axes_camera(&self, axes_index: usize) -> Option<&Camera> {
3304 self.axes_cameras.get(axes_index)
3305 }
3306
3307 pub fn scene(&self) -> &Scene {
3309 &self.scene
3310 }
3311
3312 pub fn scene_statistics(&self) -> crate::core::SceneStatistics {
3314 self.scene.statistics()
3315 }
3316
3317 pub fn view_bounds(&self) -> Option<(f64, f64, f64, f64)> {
3319 match self.camera().projection {
3320 crate::core::camera::ProjectionType::Orthographic {
3321 left,
3322 right,
3323 bottom,
3324 top,
3325 ..
3326 } => Some((left as f64, right as f64, bottom as f64, top as f64)),
3327 _ => None,
3328 }
3329 }
3330
3331 pub fn overlay_show_grid(&self) -> bool {
3333 self.figure_show_grid
3334 }
3335
3336 pub fn set_overlay_grid_enabled(&mut self, enabled: bool) {
3337 self.figure_show_grid = enabled;
3338 self.needs_update = true;
3339 }
3340
3341 pub fn overlay_show_minor_grid(&self) -> bool {
3342 self.figure_show_minor_grid
3343 }
3344 pub fn overlay_show_grid_for_axes(&self, axes_index: usize) -> bool {
3345 self.last_figure
3346 .as_ref()
3347 .and_then(|f| f.axes_metadata(axes_index))
3348 .map(|m| m.grid_enabled)
3349 .unwrap_or(self.figure_show_grid)
3350 }
3351 pub fn overlay_show_minor_grid_for_axes(&self, axes_index: usize) -> bool {
3352 Self::minor_grid_for_axes(
3353 self.last_figure.as_ref(),
3354 self.figure_show_minor_grid,
3355 axes_index,
3356 )
3357 }
3358
3359 pub fn overlay_axes_kind_for_axes(&self, axes_index: usize) -> AxesKind {
3360 self.last_figure
3361 .as_ref()
3362 .map(|f| f.axes_kind(axes_index))
3363 .unwrap_or(AxesKind::Cartesian)
3364 }
3365
3366 fn minor_grid_for_axes(
3367 last_figure: Option<&crate::plots::Figure>,
3368 figure_show_minor_grid: bool,
3369 axes_index: usize,
3370 ) -> bool {
3371 last_figure
3372 .map(|f| f.minor_grid_enabled_for_axes(axes_index))
3373 .unwrap_or(figure_show_minor_grid)
3374 }
3375
3376 fn push_vertical_grid_lines(
3377 vertices: &mut Vec<Vertex>,
3378 x_bounds: (f64, f64),
3379 y_bounds: (f64, f64),
3380 step: f64,
3381 color: Vec4,
3382 skip_major: Option<(f64, f64)>,
3383 ) {
3384 Self::for_each_grid_position(x_bounds.0, x_bounds.1, step, skip_major, |x| {
3385 let x = x as f32;
3386 let y0 = y_bounds.0 as f32;
3387 let y1 = y_bounds.1 as f32;
3388 if x.is_finite() && y0.is_finite() && y1.is_finite() {
3389 vertices.push(Vertex::new(Vec3::new(x, y0, 0.0), color));
3390 vertices.push(Vertex::new(Vec3::new(x, y1, 0.0), color));
3391 }
3392 });
3393 }
3394
3395 fn push_horizontal_grid_lines(
3396 vertices: &mut Vec<Vertex>,
3397 y_bounds: (f64, f64),
3398 x_bounds: (f64, f64),
3399 step: f64,
3400 color: Vec4,
3401 skip_major: Option<(f64, f64)>,
3402 ) {
3403 Self::for_each_grid_position(y_bounds.0, y_bounds.1, step, skip_major, |y| {
3404 let y = y as f32;
3405 let x0 = x_bounds.0 as f32;
3406 let x1 = x_bounds.1 as f32;
3407 if y.is_finite() && x0.is_finite() && x1.is_finite() {
3408 vertices.push(Vertex::new(Vec3::new(x0, y, 0.0), color));
3409 vertices.push(Vertex::new(Vec3::new(x1, y, 0.0), color));
3410 }
3411 });
3412 }
3413
3414 fn push_polar_grid_lines(
3415 vertices: &mut Vec<Vertex>,
3416 radius: f64,
3417 major_step: f64,
3418 show_major: bool,
3419 show_minor: bool,
3420 major_color: Vec4,
3421 minor_color: Vec4,
3422 ) {
3423 if !radius.is_finite() || radius <= 0.0 || !major_step.is_finite() || major_step <= 0.0 {
3424 return;
3425 }
3426
3427 if show_minor {
3428 let minor_step = (major_step / 5.0).max(f64::EPSILON);
3429 Self::for_each_grid_position(
3430 minor_step,
3431 radius,
3432 minor_step,
3433 Some((major_step, minor_step * 0.25)),
3434 |r| {
3435 Self::push_circle_grid_line(vertices, r, minor_color);
3436 },
3437 );
3438 }
3439 if show_major {
3440 Self::for_each_grid_position(major_step, radius, major_step, None, |r| {
3441 Self::push_circle_grid_line(vertices, r, major_color);
3442 });
3443 for i in 0..12 {
3444 let theta = i as f64 * std::f64::consts::TAU / 12.0;
3445 let x = (theta.cos() * radius) as f32;
3446 let y = (theta.sin() * radius) as f32;
3447 if x.is_finite() && y.is_finite() {
3448 vertices.push(Vertex::new(Vec3::ZERO, major_color));
3449 vertices.push(Vertex::new(Vec3::new(x, y, 0.0), major_color));
3450 }
3451 }
3452 }
3453 }
3454
3455 fn push_circle_grid_line(vertices: &mut Vec<Vertex>, radius: f64, color: Vec4) {
3456 if !radius.is_finite() || radius <= 0.0 {
3457 return;
3458 }
3459 const SEGMENTS: usize = 96;
3460 for i in 0..SEGMENTS {
3461 let theta0 = i as f64 * std::f64::consts::TAU / SEGMENTS as f64;
3462 let theta1 = (i + 1) as f64 * std::f64::consts::TAU / SEGMENTS as f64;
3463 let p0 = Vec3::new(
3464 (theta0.cos() * radius) as f32,
3465 (theta0.sin() * radius) as f32,
3466 0.0,
3467 );
3468 let p1 = Vec3::new(
3469 (theta1.cos() * radius) as f32,
3470 (theta1.sin() * radius) as f32,
3471 0.0,
3472 );
3473 if p0.is_finite() && p1.is_finite() {
3474 vertices.push(Vertex::new(p0, color));
3475 vertices.push(Vertex::new(p1, color));
3476 }
3477 }
3478 }
3479
3480 fn for_each_grid_position(
3481 min: f64,
3482 max: f64,
3483 step: f64,
3484 skip_major: Option<(f64, f64)>,
3485 mut visit: impl FnMut(f64),
3486 ) {
3487 if !min.is_finite() || !max.is_finite() || !step.is_finite() || step <= 0.0 || min > max {
3488 return;
3489 }
3490 let start = (min / step).ceil() * step;
3491 if !start.is_finite() {
3492 return;
3493 }
3494
3495 let mut value = start;
3496 for _ in 0..MAX_2D_GRID_LINES_PER_AXIS {
3497 if value > max {
3498 break;
3499 }
3500 let is_major = skip_major
3501 .map(|(major_step, tolerance)| {
3502 major_step.is_finite()
3503 && major_step > 0.0
3504 && (value - (value / major_step).round() * major_step).abs() <= tolerance
3505 })
3506 .unwrap_or(false);
3507 if !is_major {
3508 visit(value);
3509 }
3510
3511 let next = value + step;
3512 if !next.is_finite() || next <= value {
3513 break;
3514 }
3515 value = next;
3516 }
3517 }
3518 pub fn overlay_show_box(&self) -> bool {
3519 self.figure_show_box
3520 }
3521 pub fn overlay_show_box_for_axes(&self, axes_index: usize) -> bool {
3522 self.last_figure
3523 .as_ref()
3524 .and_then(|f| f.axes_metadata(axes_index))
3525 .map(|m| m.box_enabled)
3526 .unwrap_or(self.figure_show_box)
3527 }
3528 pub fn overlay_title(&self) -> Option<&String> {
3529 self.figure_title.as_ref()
3530 }
3531 pub fn geometry_overlay(&self) -> Option<&GeometrySceneOverlay> {
3532 self.geometry_overlay.as_ref()
3533 }
3534 pub fn geometry_xray_enabled(&self) -> bool {
3535 self.geometry_xray_enabled
3536 }
3537 pub fn geometry_scene_presentation(&self) -> &GeometryScenePresentation {
3538 &self.geometry_presentation
3539 }
3540 pub fn set_geometry_scene_presentation(&mut self, mut presentation: GeometryScenePresentation) {
3541 let requested_view_preset = presentation.view_preset.take();
3542 self.preserve_geometry_scene_state_when_unspecified(&mut presentation);
3543 if let Some(view_preset) = requested_view_preset {
3544 self.set_camera_view_preset(geometry_scene_view_preset_to_camera(view_preset));
3545 }
3546 log::info!(
3547 target: "runmat_plot",
3548 "geometry_scene.presentation_renderer selected_region_id={} selected_region_count={} has_scene={} unchanged={}",
3549 presentation.selected_region_id.as_deref().unwrap_or("none"),
3550 presentation.selected_region_ids.len(),
3551 self.last_geometry_scene.is_some(),
3552 self.geometry_presentation == presentation,
3553 );
3554 if self.geometry_presentation == presentation {
3555 return;
3556 }
3557 self.geometry_presentation = presentation;
3558 self.apply_geometry_presentation_visibility();
3559 if let Some(scene) = self.last_geometry_scene.clone() {
3560 self.refresh_geometry_scene_render_data(&scene);
3561 }
3562 }
3563 pub fn geometry_owner_visible(&self, owner_id: &str) -> bool {
3564 !self.geometry_hidden_owner_node_ids.contains(owner_id)
3565 }
3566 pub fn set_geometry_owner_visible(&mut self, owner_id: impl Into<String>, visible: bool) {
3567 let owner_id = owner_id.into();
3568 if owner_id.trim().is_empty() {
3569 return;
3570 }
3571 if visible {
3572 if !self.geometry_hidden_owner_node_ids.remove(&owner_id) {
3573 return;
3574 }
3575 } else if !self.geometry_hidden_owner_node_ids.insert(owner_id) {
3576 return;
3577 }
3578 self.geometry_presentation.hidden_owner_node_ids = Some(
3579 self.geometry_hidden_owner_node_ids
3580 .iter()
3581 .cloned()
3582 .collect(),
3583 );
3584 self.geometry_presentation.isolated_owner_node_ids = None;
3585 self.apply_geometry_visibility_filter();
3586 self.needs_update = true;
3587 }
3588 fn refresh_geometry_scene_render_data(&mut self, geometry_scene: &GeometryScene) {
3589 log::info!(
3590 target: "runmat_plot",
3591 "geometry_scene.refresh_render_data chunks={} selected_region_id={} selected_region_count={}",
3592 geometry_scene.chunks.len(),
3593 self.geometry_presentation
3594 .selected_region_id
3595 .as_deref()
3596 .unwrap_or("none"),
3597 self.geometry_presentation.selected_region_ids.len(),
3598 );
3599 self.rebuild_geometry_scene_nodes(geometry_scene);
3600 }
3601
3602 fn rebuild_geometry_scene_nodes(&mut self, geometry_scene: &GeometryScene) {
3603 self.scene.clear();
3604 self.scene_buffer_cache.borrow_mut().clear();
3605 self.geometry_node_owner_ids.clear();
3606 for (index, chunk) in geometry_scene.chunks.iter().enumerate() {
3607 let node_id = geometry_scene.chunk_node_id(index, &chunk.chunk_id);
3608 if !chunk.owner_node_ids.is_empty() {
3609 self.geometry_node_owner_ids
3610 .insert(node_id, chunk.owner_node_ids.clone());
3611 }
3612 }
3613 self.apply_geometry_presentation_visibility();
3614 for node in geometry_scene.nodes_with_presentation(&self.geometry_presentation) {
3615 self.scene.add_node_preserving_id(node);
3616 }
3617 self.apply_geometry_xray_to_nodes();
3618 self.apply_geometry_visibility_filter();
3619 self.needs_update = true;
3620 }
3621 fn preserve_geometry_scene_state_when_unspecified(
3622 &self,
3623 presentation: &mut GeometryScenePresentation,
3624 ) {
3625 if !presentation.resolves_owner_visibility() {
3626 presentation.hidden_owner_node_ids =
3627 self.geometry_presentation.hidden_owner_node_ids.clone();
3628 presentation.isolated_owner_node_ids =
3629 self.geometry_presentation.isolated_owner_node_ids.clone();
3630 }
3631 if !presentation.resolves_section() {
3632 presentation.section = self.geometry_presentation.section.clone();
3633 }
3634 }
3635 fn apply_geometry_presentation_visibility(&mut self) {
3636 if !self.geometry_presentation.resolves_owner_visibility() {
3637 return;
3638 }
3639 let all_owner_node_ids = self
3640 .geometry_node_owner_ids
3641 .values()
3642 .flat_map(|owner_ids| owner_ids.iter().map(String::as_str));
3643 self.geometry_hidden_owner_node_ids =
3644 self.geometry_presentation.resolved_hidden_owner_node_ids(
3645 all_owner_node_ids,
3646 &self.geometry_hidden_owner_node_ids,
3647 );
3648 }
3649 fn apply_geometry_visibility_filter(&mut self) {
3650 if self.geometry_node_owner_ids.is_empty() {
3651 return;
3652 }
3653 let hidden_owner_ids = &self.geometry_hidden_owner_node_ids;
3654 let node_owner_ids = &self.geometry_node_owner_ids;
3655 self.scene.for_each_node_mut(|node| {
3656 let Some(owner_ids) = node_owner_ids.get(&node.id) else {
3657 return;
3658 };
3659 node.visible = !owner_ids
3660 .iter()
3661 .any(|owner_id| hidden_owner_ids.contains(owner_id));
3662 });
3663 }
3664 pub fn set_geometry_xray_enabled(&mut self, enabled: bool) {
3665 if self.geometry_overlay.is_none() || self.geometry_xray_enabled == enabled {
3666 return;
3667 }
3668 self.geometry_xray_enabled = enabled;
3669 if let Some(scene) = self.last_geometry_scene.clone() {
3670 self.refresh_geometry_scene_render_data(&scene);
3671 } else {
3672 self.apply_geometry_xray_to_nodes();
3673 self.scene_buffer_cache.borrow_mut().clear();
3674 self.needs_update = true;
3675 }
3676 }
3677 fn apply_geometry_xray_to_nodes(&mut self) {
3678 let alpha = if self.geometry_xray_enabled {
3679 0.38
3680 } else {
3681 1.0
3682 };
3683 self.scene.for_each_node_mut(|node| {
3684 if let Some(render_data) = node.render_data.as_mut() {
3685 if matches!(
3686 render_data.pipeline_type,
3687 crate::core::PipelineType::Triangles
3688 ) {
3689 render_data.material.albedo.w = if self.geometry_xray_enabled {
3690 render_data.material.albedo.w.min(alpha)
3691 } else {
3692 render_data.material.albedo.w.max(alpha)
3693 };
3694 render_data.material.alpha_mode = if render_data.material.albedo.w < 0.98 {
3695 crate::core::AlphaMode::Blend
3696 } else {
3697 crate::core::AlphaMode::Opaque
3698 };
3699 for vertex in &mut render_data.vertices {
3700 vertex.color[3] = if self.geometry_xray_enabled {
3701 vertex.color[3].min(alpha)
3702 } else {
3703 vertex.color[3].max(alpha)
3704 };
3705 }
3706 }
3707 }
3708 });
3709 }
3710 pub fn overlay_sg_title(&self) -> Option<&String> {
3711 self.figure_sg_title.as_ref()
3712 }
3713 pub fn overlay_sg_title_style(&self) -> &TextStyle {
3714 &self.figure_sg_title_style
3715 }
3716 pub fn overlay_title_for_axes(&self, axes_index: usize) -> Option<&String> {
3717 self.last_figure
3718 .as_ref()
3719 .and_then(|f| f.axes_metadata(axes_index))
3720 .and_then(|m| m.title.as_ref())
3721 }
3722 pub fn overlay_title_style_for_axes(&self, axes_index: usize) -> Option<&TextStyle> {
3723 self.last_figure
3724 .as_ref()
3725 .and_then(|f| f.axes_metadata(axes_index))
3726 .map(|m| &m.title_style)
3727 }
3728 pub fn overlay_subtitle_for_axes(&self, axes_index: usize) -> Option<&String> {
3729 self.last_figure
3730 .as_ref()
3731 .and_then(|f| f.axes_metadata(axes_index))
3732 .and_then(|m| m.subtitle.as_ref())
3733 }
3734 pub fn overlay_subtitle_style_for_axes(&self, axes_index: usize) -> Option<&TextStyle> {
3735 self.last_figure
3736 .as_ref()
3737 .and_then(|f| f.axes_metadata(axes_index))
3738 .map(|m| &m.subtitle_style)
3739 }
3740 pub fn overlay_x_label(&self) -> Option<&String> {
3741 self.figure_x_label.as_ref()
3742 }
3743 pub fn overlay_x_label_for_axes(&self, axes_index: usize) -> Option<&String> {
3744 self.last_figure
3745 .as_ref()
3746 .and_then(|f| f.axes_metadata(axes_index))
3747 .and_then(|m| m.x_label.as_ref())
3748 }
3749 pub fn overlay_x_label_style_for_axes(&self, axes_index: usize) -> Option<&TextStyle> {
3750 self.last_figure
3751 .as_ref()
3752 .and_then(|f| f.axes_metadata(axes_index))
3753 .map(|m| &m.x_label_style)
3754 }
3755 pub fn overlay_axes_style_for_axes(&self, axes_index: usize) -> Option<&TextStyle> {
3756 self.last_figure
3757 .as_ref()
3758 .and_then(|f| f.axes_metadata(axes_index))
3759 .map(|m| &m.axes_style)
3760 }
3761 pub fn overlay_y_label(&self) -> Option<&String> {
3762 self.figure_y_label.as_ref()
3763 }
3764 pub fn overlay_y_label_for_axes(&self, axes_index: usize) -> Option<&String> {
3765 self.last_figure
3766 .as_ref()
3767 .and_then(|f| f.axes_metadata(axes_index))
3768 .and_then(|m| m.y_label.as_ref())
3769 }
3770 pub fn overlay_y_label_style_for_axes(&self, axes_index: usize) -> Option<&TextStyle> {
3771 self.last_figure
3772 .as_ref()
3773 .and_then(|f| f.axes_metadata(axes_index))
3774 .map(|m| &m.y_label_style)
3775 }
3776 pub fn overlay_z_label(&self) -> Option<&String> {
3777 self.figure_z_label.as_ref()
3778 }
3779 pub fn overlay_z_label_for_axes(&self, axes_index: usize) -> Option<&String> {
3780 self.last_figure
3781 .as_ref()
3782 .and_then(|f| f.axes_metadata(axes_index))
3783 .and_then(|m| m.z_label.as_ref())
3784 }
3785 pub fn overlay_z_label_style_for_axes(&self, axes_index: usize) -> Option<&TextStyle> {
3786 self.last_figure
3787 .as_ref()
3788 .and_then(|f| f.axes_metadata(axes_index))
3789 .map(|m| &m.z_label_style)
3790 }
3791 pub fn active_axes_pie_labels(&self) -> Vec<(String, glam::Vec2)> {
3792 let Some(fig) = self.last_figure.as_ref() else {
3793 return Vec::new();
3794 };
3795 fig.pie_labels_for_axes(fig.active_axes_index)
3796 .into_iter()
3797 .map(|entry| (entry.label, entry.position))
3798 .collect()
3799 }
3800 pub fn pie_labels_for_axes(&self, axes_index: usize) -> Vec<(String, glam::Vec2)> {
3801 let Some(fig) = self.last_figure.as_ref() else {
3802 return Vec::new();
3803 };
3804 fig.pie_labels_for_axes(axes_index)
3805 .into_iter()
3806 .map(|entry| (entry.label, entry.position))
3807 .collect()
3808 }
3809
3810 pub fn world_text_annotations_for_axes(
3811 &self,
3812 axes_index: usize,
3813 ) -> Vec<(glam::Vec3, String, TextStyle)> {
3814 self.last_figure
3815 .as_ref()
3816 .map(|f| {
3817 f.axes_text_annotations(axes_index)
3818 .iter()
3819 .map(|annotation| {
3820 (
3821 annotation.position,
3822 annotation.text.clone(),
3823 annotation.style.clone(),
3824 )
3825 })
3826 .collect()
3827 })
3828 .unwrap_or_default()
3829 }
3830
3831 pub fn world_axis_label_annotations_for_axes(
3832 &self,
3833 axes_index: usize,
3834 ) -> Vec<(glam::Vec3, String, TextStyle)> {
3835 let Some(fig) = self.last_figure.as_ref() else {
3836 return Vec::new();
3837 };
3838 let Some(meta) = fig.axes_metadata(axes_index) else {
3839 return Vec::new();
3840 };
3841 let Some(bounds) = self.display_bounds_3d_for_axes(axes_index) else {
3842 return Vec::new();
3843 };
3844 let dx = (bounds.max.x - bounds.min.x).abs().max(1.0e-3);
3845 let dy = (bounds.max.y - bounds.min.y).abs().max(1.0e-3);
3846 let dz = (bounds.max.z - bounds.min.z).abs().max(1.0e-3);
3847 let camera = self
3848 .axes_camera(axes_index)
3849 .or_else(|| Some(self.camera()))
3850 .expect("plot renderer must always have a camera");
3851 let center = (bounds.min + bounds.max) * 0.5;
3852 let cam_delta = camera.position - center;
3853 let sx = if cam_delta.x >= 0.0 { 1.0 } else { -1.0 };
3854 let sy = if cam_delta.y >= 0.0 { 1.0 } else { -1.0 };
3855 let sz = if cam_delta.z >= 0.0 { 1.0 } else { -1.0 };
3856 let x_anchor = glam::Vec3::new(bounds.min.x + dx * 0.82, bounds.min.y, bounds.min.z)
3857 + glam::Vec3::new(0.0, -sy * dy * 0.10, -sz * dz * 0.08);
3858 let y_anchor = glam::Vec3::new(bounds.min.x, bounds.min.y + dy * 0.82, bounds.min.z)
3859 + glam::Vec3::new(-sx * dx * 0.10, 0.0, -sz * dz * 0.08);
3860 let z_anchor = glam::Vec3::new(bounds.min.x, bounds.min.y, bounds.min.z + dz * 0.82)
3861 + glam::Vec3::new(-sx * dx * 0.08, -sy * dy * 0.08, 0.0);
3862 let mut out = Vec::new();
3863 if let Some(label) = meta.x_label.clone().filter(|s| !s.is_empty()) {
3864 out.push((x_anchor, label, meta.x_label_style.clone()));
3865 }
3866 if let Some(label) = meta.y_label.clone().filter(|s| !s.is_empty()) {
3867 out.push((y_anchor, label, meta.y_label_style.clone()));
3868 }
3869 if let Some(label) = meta.z_label.clone().filter(|s| !s.is_empty()) {
3870 out.push((z_anchor, label, meta.z_label_style.clone()));
3871 }
3872 out
3873 }
3874 pub fn overlay_show_legend(&self) -> bool {
3875 self.figure_show_legend
3876 }
3877 pub fn overlay_show_legend_for_axes(&self, axes_index: usize) -> bool {
3878 self.last_figure
3879 .as_ref()
3880 .and_then(|f| f.axes_metadata(axes_index))
3881 .map(|m| m.legend_enabled)
3882 .unwrap_or(self.figure_show_legend)
3883 }
3884 pub fn overlay_legend_entries(&self) -> &Vec<LegendEntry> {
3885 &self.legend_entries
3886 }
3887 pub fn overlay_legend_entries_for_axes(&self, axes_index: usize) -> Vec<LegendEntry> {
3888 self.last_figure
3889 .as_ref()
3890 .map(|f| f.legend_entries_for_axes(axes_index))
3891 .unwrap_or_default()
3892 }
3893 pub fn overlay_x_log(&self) -> bool {
3894 self.figure_x_log
3895 }
3896 pub fn overlay_x_log_for_axes(&self, axes_index: usize) -> bool {
3897 self.last_figure
3898 .as_ref()
3899 .and_then(|f| f.axes_metadata(axes_index))
3900 .map(|m| m.x_log)
3901 .unwrap_or(self.figure_x_log)
3902 }
3903 pub fn overlay_y_log(&self) -> bool {
3904 self.figure_y_log
3905 }
3906 pub fn overlay_y_log_for_axes(&self, axes_index: usize) -> bool {
3907 self.last_figure
3908 .as_ref()
3909 .and_then(|f| f.axes_metadata(axes_index))
3910 .map(|m| m.y_log)
3911 .unwrap_or(self.figure_y_log)
3912 }
3913 pub fn overlay_colormap(&self) -> ColorMap {
3914 self.figure_colormap.clone()
3915 }
3916 pub fn overlay_colorbar_enabled(&self) -> bool {
3917 self.figure_colorbar_enabled
3918 }
3919 pub fn figure_axes_grid(&self) -> (usize, usize) {
3921 self.last_figure
3922 .as_ref()
3923 .map(|f| f.axes_grid())
3924 .unwrap_or((1, 1))
3925 }
3926 pub fn figure_axes_count(&self) -> usize {
3927 self.last_figure
3928 .as_ref()
3929 .map(|f| f.axes_count())
3930 .unwrap_or(1)
3931 }
3932 pub fn overlay_parent_for_axes(&self, axes_index: usize) -> Option<usize> {
3933 self.last_figure
3934 .as_ref()
3935 .and_then(|f| f.axes_overlay_parent(axes_index))
3936 }
3937 pub fn axes_position_explicit_for_axes(&self, axes_index: usize) -> bool {
3938 self.last_figure
3939 .as_ref()
3940 .and_then(|f| f.axes_metadata(axes_index))
3941 .map(|m| m.position_explicit)
3942 .unwrap_or(false)
3943 }
3944 pub fn overlay_position_for_axes(&self, axes_index: usize) -> Option<([f64; 4], String)> {
3945 self.last_figure
3946 .as_ref()
3947 .and_then(|f| f.axes_metadata(axes_index))
3948 .filter(|m| m.position_explicit)
3949 .map(|m| (m.position, m.units.clone()))
3950 }
3951 pub fn overlay_y_axis_location_for_axes(&self, axes_index: usize) -> &str {
3952 self.last_figure
3953 .as_ref()
3954 .and_then(|f| f.axes_metadata(axes_index))
3955 .map(|m| m.y_axis_location.as_str())
3956 .unwrap_or("left")
3957 }
3958 pub fn overlay_categorical_labels(&self) -> Option<(bool, &Vec<String>)> {
3960 if let (Some(is_x), Some(labels)) = (
3961 &self.figure_categorical_is_x,
3962 &self.figure_categorical_labels,
3963 ) {
3964 Some((*is_x, labels))
3965 } else {
3966 None
3967 }
3968 }
3969
3970 pub fn overlay_categorical_labels_for_axes(
3971 &self,
3972 axes_index: usize,
3973 ) -> Option<(bool, Vec<String>)> {
3974 self.last_figure
3975 .as_ref()
3976 .and_then(|f| f.categorical_axis_labels_for_axes(axes_index))
3977 }
3978
3979 pub fn overlay_x_tick_labels_for_axes(&self, axes_index: usize) -> Option<Vec<String>> {
3980 self.last_figure
3981 .as_ref()
3982 .and_then(|f| f.x_axis_tick_labels_for_axes(axes_index))
3983 }
3984
3985 pub fn overlay_y_tick_labels_for_axes(&self, axes_index: usize) -> Option<Vec<String>> {
3986 self.last_figure
3987 .as_ref()
3988 .and_then(|f| f.y_axis_tick_labels_for_axes(axes_index))
3989 }
3990
3991 pub fn overlay_x_tick_format_for_axes(&self, axes_index: usize) -> Option<String> {
3992 self.last_figure
3993 .as_ref()
3994 .and_then(|f| f.x_axis_tick_format_for_axes(axes_index))
3995 }
3996
3997 pub fn overlay_y_tick_format_for_axes(&self, axes_index: usize) -> Option<String> {
3998 self.last_figure
3999 .as_ref()
4000 .and_then(|f| f.y_axis_tick_format_for_axes(axes_index))
4001 }
4002
4003 pub fn overlay_x_tick_label_rotation_for_axes(&self, axes_index: usize) -> f64 {
4004 self.last_figure
4005 .as_ref()
4006 .and_then(|f| f.x_axis_tick_label_rotation_for_axes(axes_index))
4007 .unwrap_or(0.0)
4008 }
4009
4010 pub fn overlay_y_tick_label_rotation_for_axes(&self, axes_index: usize) -> f64 {
4011 self.last_figure
4012 .as_ref()
4013 .and_then(|f| f.y_axis_tick_label_rotation_for_axes(axes_index))
4014 .unwrap_or(0.0)
4015 }
4016
4017 pub fn overlay_x_ticks_for_axes(&self, axes_index: usize) -> Option<Vec<f64>> {
4018 self.last_figure
4019 .as_ref()
4020 .and_then(|f| f.x_axis_ticks_for_axes(axes_index))
4021 }
4022
4023 pub fn overlay_y_ticks_for_axes(&self, axes_index: usize) -> Option<Vec<f64>> {
4024 self.last_figure
4025 .as_ref()
4026 .and_then(|f| f.y_axis_ticks_for_axes(axes_index))
4027 }
4028
4029 pub fn overlay_histogram_edges_for_axes(&self, axes_index: usize) -> Option<(bool, Vec<f64>)> {
4030 self.last_figure
4031 .as_ref()
4032 .and_then(|f| f.histogram_axis_edges_for_axes(axes_index))
4033 }
4034
4035 pub fn overlay_display_bounds_for_axes(
4037 &self,
4038 axes_index: usize,
4039 ) -> Option<(f64, f64, f64, f64)> {
4040 self.display_bounds_for_axes(axes_index)
4041 }
4042
4043 pub fn data_bounds(&self) -> Option<(f64, f64, f64, f64)> {
4045 let base = self.data_bounds;
4046 base.map(|(bx_min, bx_max, by_min, by_max)| {
4047 let (mut x_min, mut x_max) = (bx_min, bx_max);
4048 let (mut y_min, mut y_max) = (by_min, by_max);
4049 if let Some((xl, xr)) = self.figure_x_limits {
4050 x_min = xl;
4051 x_max = xr;
4052 }
4053 if let Some((yl, yr)) = self.figure_y_limits {
4054 y_min = yl;
4055 y_max = yr;
4056 }
4057 (x_min, x_max, y_min, y_max)
4058 })
4059 }
4060
4061 pub fn axes_camera_mut(&mut self, idx: usize) -> Option<&mut Camera> {
4063 self.axes_cameras.get_mut(idx)
4064 }
4065
4066 pub fn view_bounds_for_axes(&self, idx: usize) -> Option<(f64, f64, f64, f64)> {
4068 if let Some(cam) = self.axes_cameras.get(idx) {
4069 if let crate::core::camera::ProjectionType::Orthographic {
4070 left,
4071 right,
4072 bottom,
4073 top,
4074 ..
4075 } = cam.projection
4076 {
4077 return Some((left as f64, right as f64, bottom as f64, top as f64));
4078 }
4079 }
4080 None
4081 }
4082
4083 pub fn axes_bounds(&self, axes_index: usize) -> Option<crate::core::BoundingBox> {
4084 let mut min = Vec3::new(f32::INFINITY, f32::INFINITY, f32::INFINITY);
4085 let mut max = Vec3::new(f32::NEG_INFINITY, f32::NEG_INFINITY, f32::NEG_INFINITY);
4086 let mut saw_any = false;
4087
4088 for node in self.scene.get_visible_nodes() {
4089 if node.axes_index != axes_index {
4090 continue;
4091 }
4092 let Some(render_data) = &node.render_data else {
4093 continue;
4094 };
4095 if let Some(bounds) = render_data.bounds {
4096 min = min.min(bounds.min);
4097 max = max.max(bounds.max);
4098 saw_any = true;
4099 continue;
4100 }
4101 for v in &render_data.vertices {
4102 let p = Vec3::new(v.position[0], v.position[1], v.position[2]);
4103 min = min.min(p);
4104 max = max.max(p);
4105 saw_any = true;
4106 }
4107 }
4108
4109 if !saw_any {
4110 return None;
4111 }
4112 Some(crate::core::BoundingBox { min, max })
4113 }
4114
4115 pub fn export_figure_clone(&self) -> crate::plots::Figure {
4117 if let Some(f) = &self.last_figure {
4118 return f.clone();
4119 }
4120 let mut fig = crate::plots::Figure::new();
4122 fig.title = self.figure_title.clone();
4123 fig.sg_title = self.figure_sg_title.clone();
4124 fig.sg_title_style = self.figure_sg_title_style.clone();
4125 fig.x_label = self.figure_x_label.clone();
4126 fig.y_label = self.figure_y_label.clone();
4127 fig.legend_enabled = self.figure_show_legend;
4128 fig.grid_enabled = self.figure_show_grid;
4129 fig.minor_grid_enabled = self.figure_show_minor_grid;
4130 fig.box_enabled = self.figure_show_box;
4131 fig.x_limits = self.figure_x_limits;
4132 fig.y_limits = self.figure_y_limits;
4133 fig.x_log = self.figure_x_log;
4134 fig.y_log = self.figure_y_log;
4135 fig.axis_equal = self.figure_axis_equal;
4136 fig.colormap = self.figure_colormap.clone();
4137 fig.colorbar_enabled = self.figure_colorbar_enabled;
4138 let (rows, cols) = self.figure_axes_grid();
4139 fig.set_subplot_grid(rows, cols);
4140 fig
4141 }
4142}
4143
4144fn geometry_scene_view_preset_to_camera(preset: GeometrySceneViewPreset) -> CameraViewPreset {
4145 match preset {
4146 GeometrySceneViewPreset::Perspective | GeometrySceneViewPreset::Isometric => {
4147 CameraViewPreset::Perspective
4148 }
4149 GeometrySceneViewPreset::Front => CameraViewPreset::Front,
4150 GeometrySceneViewPreset::Back => CameraViewPreset::Back,
4151 GeometrySceneViewPreset::Left => CameraViewPreset::Left,
4152 GeometrySceneViewPreset::Right => CameraViewPreset::Right,
4153 GeometrySceneViewPreset::Top => CameraViewPreset::Top,
4154 GeometrySceneViewPreset::Bottom => CameraViewPreset::Bottom,
4155 }
4156}
4157
4158fn data_aspect_adjusted_bounds(
4159 x_min: f64,
4160 x_max: f64,
4161 y_min: f64,
4162 y_max: f64,
4163 ratio: [f64; 3],
4164) -> (f64, f64, f64, f64) {
4165 let x_ratio = ratio[0].abs().max(1.0e-12);
4166 let y_ratio = ratio[1].abs().max(1.0e-12);
4167 let cx = (x_min + x_max) * 0.5;
4168 let cy = (y_min + y_max) * 0.5;
4169 let x_span = (x_max - x_min).abs().max(0.1);
4170 let y_span = (y_max - y_min).abs().max(0.1);
4171 let units = (x_span / x_ratio).max(y_span / y_ratio).max(0.1);
4172 let next_x = units * x_ratio;
4173 let next_y = units * y_ratio;
4174 (
4175 cx - next_x * 0.5,
4176 cx + next_x * 0.5,
4177 cy - next_y * 0.5,
4178 cy + next_y * 0.5,
4179 )
4180}
4181
4182fn data_aspect_adjusted_bounds_3d(min: Vec3, max: Vec3, ratio: [f64; 3]) -> (Vec3, Vec3) {
4183 let aspect = Vec3::new(
4184 ratio[0].abs().max(1.0e-12) as f32,
4185 ratio[1].abs().max(1.0e-12) as f32,
4186 ratio[2].abs().max(1.0e-12) as f32,
4187 );
4188 let center = (min + max) * 0.5;
4189 let span = (max - min).abs().max(Vec3::splat(0.1));
4190 let units = (span.x / aspect.x)
4191 .max(span.y / aspect.y)
4192 .max(span.z / aspect.z)
4193 .max(0.1);
4194 let next = aspect * units;
4195 (center - next * 0.5, center + next * 0.5)
4196}
4197
4198#[cfg(test)]
4199mod tests {
4200 use super::*;
4201 use crate::plots::{figure::PlotElement, Figure, PatchPlot};
4202
4203 fn patch_element(vertices: Vec<Vec3>) -> PlotElement {
4204 PlotElement::Patch(PatchPlot::new(vertices, vec![vec![0, 1, 2]]).unwrap())
4205 }
4206
4207 #[test]
4208 fn patch_3d_detection_preserves_small_scene_depth() {
4209 let patch = patch_element(vec![
4210 Vec3::new(0.0, 0.0, 0.0),
4211 Vec3::new(1.0e-4, 0.0, 0.0),
4212 Vec3::new(0.0, 1.0e-4, 1.0e-8),
4213 ]);
4214
4215 assert!(PlotRenderer::plot_element_is_3d(&patch));
4216 }
4217
4218 #[test]
4219 fn patch_3d_detection_ignores_large_scene_relative_noise() {
4220 let patch = patch_element(vec![
4221 Vec3::new(0.0, 0.0, 0.0),
4222 Vec3::new(1.0e6, 0.0, 0.0),
4223 Vec3::new(0.0, 1.0e6, 0.5),
4224 ]);
4225
4226 assert!(!PlotRenderer::plot_element_is_3d(&patch));
4227 }
4228
4229 #[test]
4230 fn applies_z_limits_to_3d_display_bounds() {
4231 let mut figure = Figure::new();
4232 figure.set_axes_z_limits(0, Some((-2.0, 3.0)));
4233 let bounds = BoundingBox::new(Vec3::new(-1.0, -1.0, -10.0), Vec3::new(1.0, 1.0, 10.0));
4234
4235 let limited = PlotRenderer::apply_3d_display_limits_to_bounds(bounds, Some(&figure), 0);
4236
4237 assert_eq!(limited.min.z, -2.0);
4238 assert_eq!(limited.max.z, 3.0);
4239 assert_eq!(limited.min.x, -1.0);
4240 assert_eq!(limited.max.x, 1.0);
4241 }
4242
4243 #[test]
4244 fn applies_data_aspect_ratio_to_3d_display_bounds() {
4245 let mut figure = Figure::new();
4246 figure.set_axes_data_aspect_ratio(0, [1.0, 2.0, 4.0], "manual");
4247 let bounds = BoundingBox::new(Vec3::ZERO, Vec3::new(1.0, 2.0, 1.0));
4248
4249 let display = PlotRenderer::apply_3d_display_limits_to_bounds(bounds, Some(&figure), 0);
4250
4251 assert_eq!(display.min, Vec3::new(0.0, 0.0, -1.5));
4252 assert_eq!(display.max, Vec3::new(1.0, 2.0, 2.5));
4253 }
4254
4255 #[test]
4256 fn degenerate_z_bounds_remain_finite_for_3d_display_bounds() {
4257 let figure = Figure::new();
4258 let bounds = BoundingBox::new(Vec3::new(-2.0, -2.0, 0.0), Vec3::new(2.0, 2.0, 0.0));
4259
4260 let display = PlotRenderer::apply_3d_display_limits_to_bounds(bounds, Some(&figure), 0);
4261
4262 assert!(PlotRenderer::bounds_are_finite(display));
4263 assert_eq!(display.min.z, 0.0);
4264 assert_eq!(display.max.z, 0.0);
4265 }
4266
4267 #[test]
4268 fn geometry_scene_view_presets_map_to_camera_presets() {
4269 assert_eq!(
4270 geometry_scene_view_preset_to_camera(GeometrySceneViewPreset::Isometric),
4271 CameraViewPreset::Perspective
4272 );
4273 assert_eq!(
4274 geometry_scene_view_preset_to_camera(GeometrySceneViewPreset::Front),
4275 CameraViewPreset::Front
4276 );
4277 assert_eq!(
4278 geometry_scene_view_preset_to_camera(GeometrySceneViewPreset::Top),
4279 CameraViewPreset::Top
4280 );
4281 }
4282
4283 #[test]
4284 fn positive_log_bounds_preserve_valid_positive_range() {
4285 assert_eq!(PlotRenderer::positive_log_bounds(1.0, 100.0), (1.0, 100.0));
4286 }
4287
4288 #[test]
4289 fn positive_log_bounds_recover_from_nonpositive_lower_bound() {
4290 let (lo, hi) = PlotRenderer::positive_log_bounds(-10.0, 100.0);
4291
4292 assert!(lo > 0.0);
4293 assert_eq!(hi, 100.0);
4294 assert!(lo < hi);
4295 }
4296
4297 #[test]
4298 fn axes_view_contract_tracks_subplot_limits() {
4299 let mut base = Figure::new();
4300 base.set_subplot_grid(2, 1);
4301 let mut limited = base.clone();
4302 limited.set_axes_limits(0, Some((0.0, 30.0)), None);
4303 limited.set_axes_limits(1, Some((200.0, 450.0)), None);
4304
4305 let base_contract = PlotRenderer::axes_view_contract_for_figure(&base);
4306 let limited_contract = PlotRenderer::axes_view_contract_for_figure(&limited);
4307
4308 assert_ne!(base_contract, limited_contract);
4309 assert_eq!(limited_contract.axes[0].x_limits, Some((0.0, 30.0)));
4310 assert_eq!(limited_contract.axes[1].x_limits, Some((200.0, 450.0)));
4311 }
4312
4313 #[test]
4314 fn figure_level_minor_grid_survives_default_axes_metadata() {
4315 let mut figure = Figure::new();
4316 figure.minor_grid_enabled = true;
4317
4318 assert!(!figure.axes_metadata(0).unwrap().minor_grid_enabled);
4319 assert!(PlotRenderer::minor_grid_for_axes(
4320 Some(&figure),
4321 figure.minor_grid_enabled,
4322 0
4323 ));
4324 }
4325
4326 #[test]
4327 fn explicit_axes_minor_grid_false_overrides_figure_level_minor_grid() {
4328 let mut figure = Figure::new();
4329 figure.minor_grid_enabled = true;
4330 figure.set_axes_minor_grid_enabled(0, false);
4331
4332 let meta = figure.axes_metadata(0).unwrap();
4333 assert!(!meta.minor_grid_enabled);
4334 assert!(meta.minor_grid_explicit);
4335 assert!(!PlotRenderer::minor_grid_for_axes(
4336 Some(&figure),
4337 figure.minor_grid_enabled,
4338 0
4339 ));
4340 }
4341}
4342
4343pub mod plot_utils {
4345 pub fn generate_major_ticks(min: f64, max: f64) -> Vec<f64> {
4346 if !(min.is_finite() && max.is_finite()) || max <= min {
4347 return Vec::new();
4348 }
4349 let range = (max - min).max(1e-9);
4350 let step = calculate_tick_interval(range);
4351 if !(step.is_finite() && step > 0.0) {
4352 return Vec::new();
4353 }
4354
4355 let mut ticks = Vec::new();
4356 let mut value = (min / step).ceil() * step;
4357 let epsilon = range * 1e-6 + step * 1e-6;
4358 while value <= max + epsilon {
4359 let snapped = if value.abs() < epsilon { 0.0 } else { value };
4360 ticks.push(snapped);
4361 value += step;
4362 if ticks.len() > 64 {
4363 break;
4364 }
4365 }
4366
4367 let endpoint_tol = step * 0.18;
4368 let near_min = ticks.iter().any(|t| (*t - min).abs() <= endpoint_tol);
4369 let near_max = ticks.iter().any(|t| (*t - max).abs() <= endpoint_tol);
4370 if !near_min {
4371 ticks.insert(0, min);
4372 }
4373 if !near_max {
4374 ticks.push(max);
4375 }
4376
4377 ticks.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
4378 ticks.dedup_by(|a, b| (*a - *b).abs() <= endpoint_tol * 0.5);
4379 ticks
4380 }
4381
4382 pub fn calculate_tick_interval(range: f64) -> f64 {
4384 let magnitude = 10.0_f64.powf(range.log10().floor());
4385 let normalized = range / magnitude;
4386
4387 let nice_interval = if normalized <= 1.0 {
4388 0.2
4389 } else if normalized <= 2.0 {
4390 0.5
4391 } else if normalized <= 5.0 {
4392 1.0
4393 } else {
4394 2.0
4395 };
4396
4397 nice_interval * magnitude
4398 }
4399
4400 pub fn format_tick_label(value: f64) -> String {
4402 fn trim_fixed(mut s: String) -> String {
4403 if s.contains('.') {
4404 while s.ends_with('0') {
4405 s.pop();
4406 }
4407 if s.ends_with('.') {
4408 s.pop();
4409 }
4410 }
4411 if s == "-0" {
4412 "0".to_string()
4413 } else {
4414 s
4415 }
4416 }
4417
4418 if value.abs() < 0.001 {
4419 "0".to_string()
4420 } else if value.abs() >= 1000.0 || value.fract().abs() < 0.0005 {
4421 format!("{value:.0}")
4422 } else if value.abs() < 0.1 {
4423 trim_fixed(format!("{value:.3}"))
4424 } else if value.abs() < 10.0 {
4425 trim_fixed(format!("{value:.2}"))
4426 } else {
4427 trim_fixed(format!("{value:.1}"))
4428 }
4429 }
4430
4431 pub fn canonical_tick_label_format(format: &str) -> String {
4432 match format.trim().to_ascii_lowercase().as_str() {
4433 "auto" => "%g".to_string(),
4434 "usd" => "$%,.2f".to_string(),
4435 "eur" => "\u{20AC}%,.2f".to_string(),
4436 "gbp" => "\u{00A3}%,.2f".to_string(),
4437 "jpy" => "\u{00A5}%,d".to_string(),
4438 "degrees" => "%g\u{00B0}".to_string(),
4439 "percentage" => "%g%%".to_string(),
4440 _ => format.to_string(),
4441 }
4442 }
4443
4444 pub fn format_tick_label_with_format(value: f64, format: Option<&str>) -> String {
4445 TickLabelFormatter::new(format).format(value)
4446 }
4447
4448 #[derive(Debug, Clone)]
4449 pub struct TickLabelFormatter {
4450 spec: Option<NumericTickFormat>,
4451 }
4452
4453 impl TickLabelFormatter {
4454 pub fn new(format: Option<&str>) -> Self {
4455 let spec = format
4456 .map(canonical_tick_label_format)
4457 .and_then(|format| NumericTickFormat::parse(&format));
4458 Self { spec }
4459 }
4460
4461 pub fn format(&self, value: f64) -> String {
4462 let Some(spec) = &self.spec else {
4463 return format_tick_label(value);
4464 };
4465 spec.format(value)
4466 }
4467 }
4468
4469 #[derive(Debug, Clone)]
4470 struct NumericTickFormat {
4471 prefix: String,
4472 suffix: String,
4473 flags: TickFormatFlags,
4474 width: Option<usize>,
4475 precision: Option<usize>,
4476 conversion: char,
4477 }
4478
4479 #[derive(Debug, Clone, Copy, Default)]
4480 struct TickFormatFlags {
4481 thousands: bool,
4482 plus: bool,
4483 zero_pad: bool,
4484 left: bool,
4485 alternate: bool,
4486 }
4487
4488 impl NumericTickFormat {
4489 fn parse(format: &str) -> Option<Self> {
4490 let chars: Vec<char> = format.chars().collect();
4491 let mut index = 0usize;
4492 while index < chars.len() {
4493 if chars[index] == '%' {
4494 if chars.get(index + 1) == Some(&'%') {
4495 index += 2;
4496 continue;
4497 }
4498 break;
4499 }
4500 index += 1;
4501 }
4502 if index >= chars.len() {
4503 return None;
4504 }
4505
4506 let prefix = decode_percent_literals(&chars[..index]);
4507 index += 1;
4508 let mut flags = TickFormatFlags::default();
4509 while index < chars.len() {
4510 match chars[index] {
4511 ',' => flags.thousands = true,
4512 '+' => flags.plus = true,
4513 '0' => flags.zero_pad = true,
4514 '-' => flags.left = true,
4515 '#' => flags.alternate = true,
4516 _ => break,
4517 }
4518 index += 1;
4519 }
4520
4521 let width_start = index;
4522 while index < chars.len() && chars[index].is_ascii_digit() {
4523 index += 1;
4524 }
4525 let width = parse_usize(&chars[width_start..index]);
4526
4527 let precision = if chars.get(index) == Some(&'.') {
4528 index += 1;
4529 let precision_start = index;
4530 while index < chars.len() && chars[index].is_ascii_digit() {
4531 index += 1;
4532 }
4533 parse_usize(&chars[precision_start..index]).or(Some(0))
4534 } else {
4535 None
4536 };
4537
4538 let conversion = *chars.get(index)?;
4539 if !matches!(conversion, 'd' | 'i' | 'f' | 'e' | 'E' | 'g' | 'G') {
4540 return None;
4541 }
4542 index += 1;
4543 let suffix = decode_percent_literals(&chars[index..]);
4544 Some(Self {
4545 prefix,
4546 suffix,
4547 flags,
4548 width,
4549 precision,
4550 conversion,
4551 })
4552 }
4553
4554 fn format(&self, value: f64) -> String {
4555 let mut body = match self.conversion {
4556 'd' | 'i' => format_integer(value, self.precision, self.flags),
4557 'f' => format_fixed(value, self.precision.unwrap_or(6), self.flags),
4558 'e' => format_exponential(value, self.precision.unwrap_or(6), false),
4559 'E' => format_exponential(value, self.precision.unwrap_or(6), true),
4560 'g' => format_general(value, self.precision.unwrap_or(6), false, self.flags),
4561 'G' => format_general(value, self.precision.unwrap_or(6), true, self.flags),
4562 _ => format_tick_label(value),
4563 };
4564 if self.flags.plus && value.is_sign_positive() && !body.starts_with('+') {
4565 body.insert(0, '+');
4566 }
4567 if self.flags.thousands {
4568 body = add_thousands_separators(&body);
4569 }
4570 body = apply_width(body, self.width, self.flags);
4571 format!("{}{}{}", self.prefix, body, self.suffix)
4572 }
4573 }
4574
4575 fn parse_usize(chars: &[char]) -> Option<usize> {
4576 if chars.is_empty() {
4577 return None;
4578 }
4579 chars.iter().collect::<String>().parse().ok()
4580 }
4581
4582 fn decode_percent_literals(chars: &[char]) -> String {
4583 let mut out = String::new();
4584 let mut index = 0usize;
4585 while index < chars.len() {
4586 if chars[index] == '%' && chars.get(index + 1) == Some(&'%') {
4587 out.push('%');
4588 index += 2;
4589 } else {
4590 out.push(chars[index]);
4591 index += 1;
4592 }
4593 }
4594 out
4595 }
4596
4597 fn format_integer(value: f64, precision: Option<usize>, flags: TickFormatFlags) -> String {
4598 let rounded = if value.is_finite() {
4599 value.round()
4600 } else {
4601 value
4602 };
4603 let mut text = format!("{rounded:.0}");
4604 if let Some(precision) = precision {
4605 let negative = text.starts_with('-');
4606 let digits = if negative { &text[1..] } else { &text };
4607 if digits.len() < precision {
4608 let mut padded = "0".repeat(precision - digits.len());
4609 padded.push_str(digits);
4610 text = if negative {
4611 format!("-{padded}")
4612 } else {
4613 padded
4614 };
4615 }
4616 }
4617 if flags.alternate && !text.contains('.') {
4618 text.push('.');
4619 }
4620 text
4621 }
4622
4623 fn format_fixed(value: f64, precision: usize, flags: TickFormatFlags) -> String {
4624 let mut text = format!("{value:.precision$}");
4625 if precision == 0 && flags.alternate && !text.contains('.') {
4626 text.push('.');
4627 }
4628 text
4629 }
4630
4631 fn format_exponential(value: f64, precision: usize, upper: bool) -> String {
4632 let text = format!("{value:.precision$e}");
4633 if upper {
4634 text.to_ascii_uppercase()
4635 } else {
4636 text
4637 }
4638 }
4639
4640 fn format_general(value: f64, precision: usize, upper: bool, flags: TickFormatFlags) -> String {
4641 if value == 0.0 {
4642 return "0".to_string();
4643 }
4644 let abs = value.abs();
4645 let use_exp = abs < 1e-4 || abs >= 10f64.powi(precision as i32);
4646 let text = if use_exp {
4647 let decimals = precision.saturating_sub(1);
4648 format_exponential(value, decimals, upper)
4649 } else {
4650 let integer_digits = abs.log10().floor().max(0.0) as usize + 1;
4651 let decimals = precision.saturating_sub(integer_digits);
4652 format_fixed(value, decimals, flags)
4653 };
4654 if flags.alternate {
4655 text
4656 } else {
4657 trim_general_zeros(text)
4658 }
4659 }
4660
4661 fn trim_general_zeros(mut text: String) -> String {
4662 let exp_index = text.find(['e', 'E']);
4663 let suffix = exp_index.map(|idx| text.split_off(idx));
4664 if text.contains('.') {
4665 while text.ends_with('0') {
4666 text.pop();
4667 }
4668 if text.ends_with('.') {
4669 text.pop();
4670 }
4671 }
4672 if let Some(suffix) = suffix {
4673 text.push_str(&suffix);
4674 }
4675 text
4676 }
4677
4678 fn add_thousands_separators(text: &str) -> String {
4679 let (sign, rest) = match text.strip_prefix('-') {
4680 Some(rest) => ("-", rest),
4681 None => match text.strip_prefix('+') {
4682 Some(rest) => ("+", rest),
4683 None => ("", text),
4684 },
4685 };
4686 let (integer, suffix) = rest
4687 .find(['.', 'e', 'E'])
4688 .map(|idx| rest.split_at(idx))
4689 .unwrap_or((rest, ""));
4690 let chars: Vec<char> = integer.chars().collect();
4691 let mut out = String::new();
4692 for (idx, ch) in chars.iter().enumerate() {
4693 if idx > 0 && (chars.len() - idx).is_multiple_of(3) {
4694 out.push(',');
4695 }
4696 out.push(*ch);
4697 }
4698 format!("{sign}{out}{suffix}")
4699 }
4700
4701 fn apply_width(mut text: String, width: Option<usize>, flags: TickFormatFlags) -> String {
4702 let Some(width) = width else {
4703 return text;
4704 };
4705 let len = text.chars().count();
4706 if len >= width {
4707 return text;
4708 }
4709 let pad = width - len;
4710 if flags.left {
4711 text.push_str(&" ".repeat(pad));
4712 text
4713 } else if flags.zero_pad {
4714 let mut chars = text.chars();
4715 match chars.next() {
4716 Some(sign @ ('-' | '+')) => {
4717 let rest = chars.collect::<String>();
4718 format!("{sign}{}{rest}", "0".repeat(pad))
4719 }
4720 _ => format!("{}{text}", "0".repeat(pad)),
4721 }
4722 } else {
4723 format!("{}{text}", " ".repeat(pad))
4724 }
4725 }
4726
4727 pub fn generate_grid_lines(
4729 bounds: (f64, f64, f64, f64),
4730 plot_rect: (f32, f32, f32, f32), ) -> Vec<(f32, f32, f32, f32)> {
4732 let (x_min, x_max, y_min, y_max) = bounds;
4734 let (left, right, bottom, top) = plot_rect;
4735
4736 let mut lines = Vec::new();
4737
4738 let x_range = x_max - x_min;
4740 let x_interval = calculate_tick_interval(x_range);
4741 let mut x_val = (x_min / x_interval).ceil() * x_interval;
4742
4743 while x_val <= x_max {
4744 let x_screen = left + ((x_val - x_min) / x_range) as f32 * (right - left);
4745 lines.push((x_screen, bottom, x_screen, top));
4746 x_val += x_interval;
4747 }
4748
4749 let y_range = y_max - y_min;
4751 let y_interval = calculate_tick_interval(y_range);
4752 let mut y_val = (y_min / y_interval).ceil() * y_interval;
4753
4754 while y_val <= y_max {
4755 let y_screen = bottom + ((y_val - y_min) / y_range) as f32 * (top - bottom);
4756 lines.push((left, y_screen, right, y_screen));
4757 y_val += y_interval;
4758 }
4759
4760 lines
4761 }
4762}