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