use crate::core::renderer::Vertex;
use crate::core::{Camera, ClipPolicy, DepthMode, Scene, WgpuRenderer};
use crate::plots::figure::{LegendEntry, TextStyle};
use crate::plots::surface::ColorMap;
use crate::plots::Figure;
use glam::{Mat4, Vec3, Vec4};
use runmat_time::Instant;
use std::cell::RefCell;
use std::collections::HashMap;
use std::sync::Arc;
#[derive(Clone, Debug)]
struct CachedSceneBuffers {
vertex_signature: (usize, usize),
vertex_buffer: Arc<wgpu::Buffer>,
index_signature: Option<(usize, usize)>,
index_buffer: Option<Arc<wgpu::Buffer>>,
}
pub struct PlotRenderer {
pub wgpu_renderer: WgpuRenderer,
pub scene: Scene,
pub theme: crate::styling::PlotThemeConfig,
data_bounds: Option<(f64, f64, f64, f64)>,
needs_update: bool,
figure_title: Option<String>,
figure_x_label: Option<String>,
figure_y_label: Option<String>,
figure_z_label: Option<String>,
figure_show_grid: bool,
figure_show_legend: bool,
figure_show_box: bool,
figure_x_limits: Option<(f64, f64)>,
figure_y_limits: Option<(f64, f64)>,
legend_entries: Vec<LegendEntry>,
figure_x_log: bool,
figure_y_log: bool,
figure_axis_equal: bool,
figure_colormap: ColorMap,
figure_colorbar_enabled: bool,
figure_categorical_is_x: Option<bool>,
figure_categorical_labels: Option<Vec<String>>,
axes_cameras: Vec<Camera>,
pub(crate) last_figure: Option<crate::plots::Figure>,
last_scene_viewport_px: Option<(u32, u32)>,
last_axes_plot_sizes_px: Option<Vec<(u32, u32)>>,
camera_auto_fit: bool,
axes_2d_camera_user_controlled: Vec<bool>,
scene_buffer_cache: RefCell<HashMap<u64, CachedSceneBuffers>>,
}
#[derive(Debug, Clone)]
pub struct PlotRenderConfig {
pub width: u32,
pub height: u32,
pub background_color: Vec4,
pub show_grid: bool,
pub show_axes: bool,
pub show_title: bool,
pub msaa_samples: u32,
pub depth_mode: DepthMode,
pub clip_policy: ClipPolicy,
pub theme: crate::styling::PlotThemeConfig,
}
impl Default for PlotRenderConfig {
fn default() -> Self {
Self {
width: 800,
height: 600,
background_color: Vec4::new(0.08, 0.09, 0.11, 1.0), show_grid: true,
show_axes: true,
show_title: true,
msaa_samples: 4,
depth_mode: DepthMode::default(),
clip_policy: ClipPolicy::default(),
theme: crate::styling::PlotThemeConfig::default(),
}
}
}
pub struct RenderTarget<'a> {
pub view: &'a wgpu::TextureView,
pub resolve_target: Option<&'a wgpu::TextureView>,
}
#[derive(Debug)]
pub struct RenderResult {
pub success: bool,
pub data_bounds: Option<(f64, f64, f64, f64)>,
pub vertex_count: usize,
pub triangle_count: usize,
pub render_time_ms: f64,
}
impl PlotRenderer {
pub fn on_surface_config_updated(&mut self) {
let current = (
self.wgpu_renderer.surface_config.width.max(1),
self.wgpu_renderer.surface_config.height.max(1),
);
if self.last_scene_viewport_px == Some(current) {
return;
}
let Some(figure) = self.last_figure.clone() else {
self.last_scene_viewport_px = Some(current);
return;
};
self.set_figure(figure);
}
fn prepare_buffers_for_render_data(
&self,
node_id: u64,
render_data: &crate::core::RenderData,
) -> Option<(Arc<wgpu::Buffer>, Option<Arc<wgpu::Buffer>>)> {
let mut cache = self.scene_buffer_cache.borrow_mut();
let vertex_signature = (
render_data.vertices.as_ptr() as usize,
render_data.vertices.len(),
);
let index_signature = render_data
.indices
.as_ref()
.map(|indices| (indices.as_ptr() as usize, indices.len()));
if let Some(cached) = cache.get(&node_id) {
if cached.vertex_signature == vertex_signature
&& cached.index_signature == index_signature
{
return Some((cached.vertex_buffer.clone(), cached.index_buffer.clone()));
}
}
let vertex_buffer = self
.wgpu_renderer
.vertex_buffer_from_sources(render_data.gpu_vertices.as_ref(), &render_data.vertices)?;
let index_buffer = render_data
.indices
.as_ref()
.map(|indices| Arc::new(self.wgpu_renderer.create_index_buffer(indices)));
cache.insert(
node_id,
CachedSceneBuffers {
vertex_signature,
vertex_buffer: vertex_buffer.clone(),
index_signature,
index_buffer: index_buffer.clone(),
},
);
Some((vertex_buffer, index_buffer))
}
fn gpu_indirect_args(render_data: &crate::core::RenderData) -> Option<(&wgpu::Buffer, u64)> {
render_data
.gpu_vertices
.as_ref()
.and_then(|buf| buf.indirect.as_ref())
.map(|indirect| (indirect.args.as_ref(), indirect.offset))
}
pub async fn new(
device: Arc<wgpu::Device>,
queue: Arc<wgpu::Queue>,
surface_config: wgpu::SurfaceConfiguration,
) -> Result<Self, Box<dyn std::error::Error>> {
let wgpu_renderer = WgpuRenderer::new(device, queue, surface_config).await;
let scene = Scene::new();
let theme = crate::styling::PlotThemeConfig::default();
Ok(Self {
wgpu_renderer,
scene,
theme,
data_bounds: None,
needs_update: true,
figure_title: None,
figure_x_label: None,
figure_y_label: None,
figure_z_label: None,
figure_show_grid: true,
figure_show_legend: true,
figure_show_box: true,
figure_x_limits: None,
figure_y_limits: None,
legend_entries: Vec::new(),
figure_x_log: false,
figure_y_log: false,
figure_axis_equal: false,
figure_colormap: ColorMap::Parula,
figure_colorbar_enabled: false,
figure_categorical_is_x: None,
figure_categorical_labels: None,
axes_cameras: vec![Self::create_default_camera()],
last_figure: None,
last_scene_viewport_px: None,
last_axes_plot_sizes_px: None,
camera_auto_fit: true,
axes_2d_camera_user_controlled: vec![false],
scene_buffer_cache: RefCell::new(HashMap::new()),
})
}
fn plot_element_is_3d(plot: &crate::plots::figure::PlotElement) -> bool {
match plot {
crate::plots::figure::PlotElement::Surface(surface) => !surface.image_mode,
crate::plots::figure::PlotElement::Line3(_) => true,
crate::plots::figure::PlotElement::Scatter3(_) => true,
_ => false,
}
}
pub fn axes_has_3d_content(&self, axes_index: usize) -> bool {
self.last_figure
.as_ref()
.map(|figure| {
figure
.plots()
.zip(figure.plot_axes_indices().iter().copied())
.any(|(plot, plot_axes_index)| {
plot_axes_index == axes_index && Self::plot_element_is_3d(plot)
})
})
.unwrap_or(false)
}
pub fn note_camera_interaction(&mut self) {
if self.camera_auto_fit {
log::debug!(target: "runmat_plot", "camera_auto_fit disabled (user interaction)");
}
self.camera_auto_fit = false;
}
pub fn note_axes_camera_interaction(&mut self, axes_index: usize) {
self.note_camera_interaction();
if self.axes_has_3d_content(axes_index) {
return;
}
if let Some(flag) = self.axes_2d_camera_user_controlled.get_mut(axes_index) {
*flag = true;
}
}
fn clear_axes_camera_interaction(&mut self, axes_index: usize) {
if let Some(flag) = self.axes_2d_camera_user_controlled.get_mut(axes_index) {
*flag = false;
}
}
fn clear_all_axes_camera_interaction(&mut self) {
for flag in &mut self.axes_2d_camera_user_controlled {
*flag = false;
}
}
pub fn set_figure(&mut self, figure: Figure) {
self.scene.clear();
self.scene_buffer_cache.borrow_mut().clear();
self.cache_figure_meta(&figure);
self.last_figure = Some(figure.clone());
self.last_axes_plot_sizes_px = None;
let (rows, cols) = figure.axes_grid();
let num_axes = rows.max(1) * cols.max(1);
if self.axes_cameras.len() != num_axes {
self.axes_cameras
.resize_with(num_axes, Self::create_default_camera);
self.axes_2d_camera_user_controlled.resize(num_axes, false);
self.camera_auto_fit = true;
}
for axes_index in 0..num_axes {
let wants_3d = self.axes_has_3d_content(axes_index);
let has_3d_camera = self
.axes_cameras
.get(axes_index)
.map(|cam| {
matches!(
cam.projection,
crate::core::camera::ProjectionType::Perspective { .. }
)
})
.unwrap_or(false);
if wants_3d != has_3d_camera {
self.axes_cameras[axes_index] = if wants_3d {
Camera::new()
} else {
Self::create_default_camera()
};
self.clear_axes_camera_interaction(axes_index);
self.camera_auto_fit = true;
}
}
self.add_figure_to_scene(figure);
self.needs_update = true;
let fit_applied = if self.camera_auto_fit {
if num_axes > 1 {
self.fit_cameras_to_axes_data()
} else {
self.fit_camera_to_data()
}
} else {
false
};
if self.camera_auto_fit && fit_applied {
self.camera_auto_fit = false;
}
self.apply_stored_axes_views();
}
fn add_figure_to_scene(&mut self, figure: Figure) {
self.add_figure_to_scene_with_axes_plot_sizes(figure, None);
}
fn add_figure_to_scene_with_axes_plot_sizes(
&mut self,
mut figure: Figure,
axes_plot_sizes_px: Option<&[(u32, u32)]>,
) {
use crate::core::SceneNode;
let viewport_px = (
self.wgpu_renderer.surface_config.width.max(1),
self.wgpu_renderer.surface_config.height.max(1),
);
self.last_scene_viewport_px = Some(viewport_px);
let gpu = crate::core::GpuPackContext {
device: &self.wgpu_renderer.device,
queue: &self.wgpu_renderer.queue,
};
let render_data_list = figure.render_data_with_axes_with_viewport_and_gpu(
Some(viewport_px),
axes_plot_sizes_px,
Some(&gpu),
);
let (rows, cols) = figure.axes_grid();
for (node_id_counter, (axes_index, render_data)) in render_data_list.into_iter().enumerate()
{
let axes_index = axes_index.min(rows * cols - 1);
let node = SceneNode {
id: node_id_counter as u64,
name: format!("Plot {node_id_counter} @axes {axes_index}"),
transform: Mat4::IDENTITY,
visible: true,
cast_shadows: false,
receive_shadows: false,
axes_index,
parent: None,
children: Vec::new(),
render_data: Some(render_data),
bounds: crate::core::BoundingBox::default(),
lod_levels: Vec::new(),
current_lod: 0,
};
let nid = self.scene.add_node(node);
let _ = nid;
let _ = axes_index;
let _ = rows;
let _ = cols;
}
}
pub fn ensure_scene_viewport_dependent_geometry_for_axes(
&mut self,
axes_plot_sizes_px: &[(u32, u32)],
) {
let normalized: Vec<(u32, u32)> = axes_plot_sizes_px
.iter()
.map(|&(w, h)| (w.max(1), h.max(1)))
.collect();
if self.last_axes_plot_sizes_px.as_ref() == Some(&normalized) {
return;
}
let Some(figure) = self.last_figure.clone() else {
self.last_axes_plot_sizes_px = Some(normalized);
return;
};
self.scene.clear();
self.scene_buffer_cache.borrow_mut().clear();
self.add_figure_to_scene_with_axes_plot_sizes(figure, Some(&normalized));
log::debug!(
target: "runmat_plot.viewport_rebuild",
"rebuilt viewport-dependent scene geometry axes_count={} viewport_sizes={:?}",
normalized.len(),
normalized
);
self.refit_2d_cameras_to_scene_bounds();
self.last_axes_plot_sizes_px = Some(normalized);
self.needs_update = true;
}
fn refit_2d_cameras_to_scene_bounds(&mut self) {
for idx in 0..self.axes_cameras.len() {
if self.axes_has_3d_content(idx) {
continue;
}
if self
.axes_2d_camera_user_controlled
.get(idx)
.copied()
.unwrap_or(false)
{
continue;
}
let Some((x_min, x_max, y_min, y_max)) = self.display_bounds_for_axes(idx) else {
continue;
};
let geometry_bounds = self.axes_bounds(idx);
let Some(cam) = self.axes_cameras.get_mut(idx) else {
continue;
};
if let crate::core::camera::ProjectionType::Orthographic {
ref mut left,
ref mut right,
ref mut bottom,
ref mut top,
..
} = cam.projection
{
*left = x_min as f32;
*right = x_max as f32;
*bottom = y_min as f32;
*top = y_max as f32;
let camera_left = *left;
let camera_right = *right;
let camera_bottom = *bottom;
let camera_top = *top;
cam.position.z = 1.0;
cam.target.z = 0.0;
cam.mark_dirty();
if let Some(bounds) = geometry_bounds {
log::debug!(
target: "runmat_plot.camera_refit",
"refit 2d camera to rebuilt scene bounds axes_index={} geometry=({}, {})..({}, {}) camera=({}, {})..({}, {}) margins=top:{} bottom:{} left:{} right:{}",
idx,
bounds.min.x,
bounds.min.y,
bounds.max.x,
bounds.max.y,
camera_left,
camera_bottom,
camera_right,
camera_top,
camera_top - bounds.max.y,
bounds.min.y - camera_bottom,
bounds.min.x - camera_left,
camera_right - bounds.max.x
);
} else {
log::debug!(
target: "runmat_plot.camera_refit",
"refit 2d camera without geometry bounds axes_index={} camera=({}, {})..({}, {})",
idx,
camera_left,
camera_bottom,
camera_right,
camera_top
);
}
if let Some(display_bounds) = self.display_bounds_for_axes(idx) {
log::debug!(
target: "runmat_plot.bounds_chain",
"bounds chain axes_index={} axes_bounds=({}, {})..({}, {}) display_bounds=({}, {})..({}, {}) camera_bounds=({}, {})..({}, {})",
idx,
geometry_bounds.map(|b| b.min.x as f64).unwrap_or(f64::NAN),
geometry_bounds.map(|b| b.min.y as f64).unwrap_or(f64::NAN),
geometry_bounds.map(|b| b.max.x as f64).unwrap_or(f64::NAN),
geometry_bounds.map(|b| b.max.y as f64).unwrap_or(f64::NAN),
display_bounds.0,
display_bounds.2,
display_bounds.1,
display_bounds.3,
camera_left,
camera_bottom,
camera_right,
camera_top
);
}
}
}
}
fn cache_figure_meta(&mut self, figure: &Figure) {
self.figure_title = figure.title.clone();
self.figure_x_label = figure.x_label.clone();
self.figure_y_label = figure.y_label.clone();
self.figure_z_label = figure.z_label.clone();
self.figure_show_grid = figure.grid_enabled;
self.figure_show_legend = figure.legend_enabled;
self.figure_show_box = figure.box_enabled;
self.figure_x_limits = figure.x_limits;
self.figure_y_limits = figure.y_limits;
self.legend_entries = figure.legend_entries();
self.figure_x_log = figure.x_log;
self.figure_y_log = figure.y_log;
self.figure_axis_equal = figure.axis_equal;
self.figure_colormap = figure.colormap;
self.figure_colorbar_enabled = figure.colorbar_enabled;
if let Some((is_x, labels)) = figure.categorical_axis_labels() {
self.figure_categorical_is_x = Some(is_x);
self.figure_categorical_labels = Some(labels);
} else {
self.figure_categorical_is_x = None;
self.figure_categorical_labels = None;
}
}
fn apply_stored_axes_views(&mut self) {
let Some(fig) = self.last_figure.as_ref() else {
return;
};
for (idx, cam) in self.axes_cameras.iter_mut().enumerate() {
if !matches!(
cam.projection,
crate::core::camera::ProjectionType::Perspective { .. }
) {
continue;
}
if let Some(meta) = fig.axes_metadata(idx) {
if let (Some(az), Some(el)) = (meta.view_azimuth_deg, meta.view_elevation_deg) {
cam.set_view_angles_deg(az, el);
}
}
}
}
fn display_bounds_for_axes(&self, axes_index: usize) -> Option<(f64, f64, f64, f64)> {
let base = self.axes_bounds(axes_index)?;
let mut x_min = base.min.x as f64;
let mut x_max = base.max.x as f64;
let mut y_min = base.min.y as f64;
let mut y_max = base.max.y as f64;
if let Some(fig) = self.last_figure.as_ref() {
if let Some(meta) = fig.axes_metadata(axes_index) {
if let Some((xl, xr)) = meta.x_limits {
x_min = xl;
x_max = xr;
}
if let Some((yl, yr)) = meta.y_limits {
y_min = yl;
y_max = yr;
}
if meta.axis_equal {
let cx = (x_min + x_max) * 0.5;
let cy = (y_min + y_max) * 0.5;
let size = (x_max - x_min).abs().max((y_max - y_min).abs()).max(0.1);
x_min = cx - size * 0.5;
x_max = cx + size * 0.5;
y_min = cy - size * 0.5;
y_max = cy + size * 0.5;
}
}
}
Some((x_min, x_max, y_min, y_max))
}
fn fit_cameras_to_axes_data(&mut self) -> bool {
let mut applied = false;
for idx in 0..self.axes_cameras.len() {
if self.axes_has_3d_content(idx) {
let Some(bounds) = self.axes_bounds(idx) else {
continue;
};
let center = (bounds.min + bounds.max) * 0.5;
let mut cam = Camera::new();
cam.target = center;
cam.up = Vec3::Z;
cam.position = center + Vec3::new(1.0, -1.0, 1.0);
cam.fit_bounds(bounds.min, bounds.max);
self.axes_cameras[idx] = cam;
applied = true;
continue;
}
let Some((x_min, x_max, y_min, y_max)) = self.display_bounds_for_axes(idx) else {
continue;
};
let mut cam = Self::create_default_camera();
if let crate::core::camera::ProjectionType::Orthographic {
ref mut left,
ref mut right,
ref mut bottom,
ref mut top,
..
} = cam.projection
{
*left = x_min as f32;
*right = x_max as f32;
*bottom = y_min as f32;
*top = y_max as f32;
}
cam.position.z = 1.0;
cam.target.z = 0.0;
cam.mark_dirty();
self.axes_cameras[idx] = cam;
applied = true;
}
applied
}
pub fn calculate_data_bounds(&mut self) -> Option<(f64, f64, f64, f64)> {
let mut min_x = f64::INFINITY;
let mut max_x = f64::NEG_INFINITY;
let mut min_y = f64::INFINITY;
let mut max_y = f64::NEG_INFINITY;
for node in self.scene.get_visible_nodes() {
if let Some(render_data) = &node.render_data {
if let Some(bounds) = render_data.bounds {
min_x = min_x.min(bounds.min.x as f64);
max_x = max_x.max(bounds.max.x as f64);
min_y = min_y.min(bounds.min.y as f64);
max_y = max_y.max(bounds.max.y as f64);
continue;
}
for vertex in &render_data.vertices {
let x = vertex.position[0] as f64;
let y = vertex.position[1] as f64;
min_x = min_x.min(x);
max_x = max_x.max(x);
min_y = min_y.min(y);
max_y = max_y.max(y);
}
}
}
if min_x != f64::INFINITY && max_x != f64::NEG_INFINITY {
let x_range = (max_x - min_x).max(0.1);
let y_range = (max_y - min_y).max(0.1);
let x_margin = x_range * 0.04;
let y_margin = y_range * 0.04;
let bounds = (
min_x - x_margin,
max_x + x_margin,
min_y - y_margin,
max_y + y_margin,
);
self.data_bounds = Some(bounds);
Some(bounds)
} else {
self.data_bounds = None;
None
}
}
pub fn fit_camera_to_data(&mut self) -> bool {
if self.axes_cameras.len() > 1 {
return self.fit_cameras_to_axes_data();
}
if self.axes_has_3d_content(0) {
let Some(bounds) = self.axes_bounds(0) else {
return false;
};
let center = (bounds.min + bounds.max) * 0.5;
let mut cam = Camera::new();
cam.target = center;
cam.up = Vec3::Z;
cam.position = center + Vec3::new(1.0, -1.0, 1.0);
cam.fit_bounds(bounds.min, bounds.max);
if let Some(axis_cam) = self.axes_cameras.get_mut(0) {
*axis_cam = cam;
}
return true;
}
if let Some((x_min, x_max, y_min, y_max)) = self.display_bounds_for_axes(0) {
let mut cam = Self::create_default_camera();
let l = x_min as f32;
let r = x_max as f32;
let b = y_min as f32;
let t = y_max as f32;
if let crate::core::camera::ProjectionType::Orthographic {
ref mut left,
ref mut right,
ref mut bottom,
ref mut top,
..
} = cam.projection
{
*left = l;
*right = r;
*bottom = b;
*top = t;
}
cam.position.z = 1.0;
cam.target.z = 0.0;
cam.mark_dirty();
if let Some(axis_cam) = self.axes_cameras.get_mut(0) {
*axis_cam = cam;
}
return true;
}
false
}
pub fn fit_extents(&mut self) {
let _ = if self.figure_axes_grid().0 * self.figure_axes_grid().1 > 1 {
self.fit_cameras_to_axes_data()
} else {
self.fit_camera_to_data()
};
self.clear_all_axes_camera_interaction();
self.camera_auto_fit = false;
self.needs_update = true;
}
pub fn reset_camera_position(&mut self) {
let dir = Vec3::new(1.0, -1.0, 1.0).normalize_or_zero();
let data_centers: Vec<Vec3> = (0..self.axes_cameras.len())
.map(|idx| {
self.axes_bounds(idx)
.map(|b| (b.min + b.max) * 0.5)
.unwrap_or_else(|| self.axes_cameras[idx].target)
})
.collect();
let display_bounds: Vec<Option<(f64, f64, f64, f64)>> = (0..self.axes_cameras.len())
.map(|idx| self.display_bounds_for_axes(idx))
.collect();
for (idx, c) in self.axes_cameras.iter_mut().enumerate() {
if matches!(
c.projection,
crate::core::camera::ProjectionType::Perspective { .. }
) {
let data_center = data_centers.get(idx).copied().unwrap_or(c.target);
let dist = (c.position - c.target).length().max(0.1);
c.target = data_center;
c.up = Vec3::Z;
c.position = data_center + dir * dist;
c.mark_dirty();
} else if let Some((x_min, x_max, y_min, y_max)) = display_bounds[idx] {
let mut cam = Self::create_default_camera();
if let crate::core::camera::ProjectionType::Orthographic {
ref mut left,
ref mut right,
ref mut bottom,
ref mut top,
..
} = cam.projection
{
*left = x_min as f32;
*right = x_max as f32;
*bottom = y_min as f32;
*top = y_max as f32;
}
cam.position.z = 1.0;
cam.target.z = 0.0;
cam.mark_dirty();
*c = cam;
}
}
self.clear_all_axes_camera_interaction();
self.camera_auto_fit = false;
self.needs_update = true;
}
pub fn render_to_viewport(
&mut self,
encoder: &mut wgpu::CommandEncoder,
target_view: &wgpu::TextureView,
_viewport: (f32, f32, f32, f32), clear_background: bool,
background_color: Option<glam::Vec4>,
) -> Result<RenderResult, Box<dyn std::error::Error>> {
let start_time = Instant::now();
let mut render_items = Vec::new();
let mut total_vertices = 0;
let mut total_triangles = 0;
for node in self.scene.get_visible_nodes() {
if let Some(render_data) = &node.render_data {
if let Some(vertex_buffer) = self.wgpu_renderer.vertex_buffer_from_sources(
render_data.gpu_vertices.as_ref(),
&render_data.vertices,
) {
self.wgpu_renderer
.ensure_pipeline(render_data.pipeline_type);
log::trace!(
target: "runmat_plot",
"upload vertices={}, draw_calls={}",
render_data.vertex_count(),
render_data.draw_calls.len()
);
render_items.push((render_data, vertex_buffer));
total_vertices += render_data.vertex_count();
if render_data.pipeline_type == crate::core::PipelineType::Triangles {
total_triangles += render_data.vertex_count() / 3;
}
}
}
}
let mut cam = self.camera().clone();
let view_proj_matrix = cam.view_proj_matrix();
self.wgpu_renderer
.update_uniforms(view_proj_matrix, Mat4::IDENTITY);
let use_msaa = self.wgpu_renderer.msaa_sample_count > 1;
let msaa_view_opt = if use_msaa {
let tex = self
.wgpu_renderer
.device
.create_texture(&wgpu::TextureDescriptor {
label: Some("runmat_msaa_color_camera"),
size: wgpu::Extent3d {
width: self.wgpu_renderer.surface_config.width,
height: self.wgpu_renderer.surface_config.height,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: self.wgpu_renderer.msaa_sample_count,
dimension: wgpu::TextureDimension::D2,
format: self.wgpu_renderer.surface_config.format,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
view_formats: &[],
});
Some(tex.create_view(&wgpu::TextureViewDescriptor::default()))
} else {
None
};
let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("Viewport Plot Render Pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: msaa_view_opt.as_ref().unwrap_or(target_view),
resolve_target: if use_msaa { Some(target_view) } else { None },
ops: wgpu::Operations {
load: if clear_background {
wgpu::LoadOp::Clear(wgpu::Color {
r: background_color.map_or(0.08, |c| c.x as f64),
g: background_color.map_or(0.09, |c| c.y as f64),
b: background_color.map_or(0.11, |c| c.z as f64),
a: background_color.map_or(1.0, |c| c.w as f64),
})
} else {
wgpu::LoadOp::Load
},
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
occlusion_query_set: None,
timestamp_writes: None,
});
let (vx, vy, vw, vh) = _viewport;
render_pass.set_viewport(vx, vy, vw, vh, 0.0, 1.0);
let sw = self.wgpu_renderer.surface_config.width as f32;
let sh = self.wgpu_renderer.surface_config.height as f32;
let ndc_left = (vx / sw) * 2.0 - 1.0;
let ndc_right = ((vx + vw) / sw) * 2.0 - 1.0;
let ndc_top = 1.0 - (vy / sh) * 2.0;
let ndc_bottom = 1.0 - ((vy + vh) / sh) * 2.0;
let (x_min, y_min, x_max, y_max) = (0.0_f64, 0.0_f64, 1.0_f64, 1.0_f64);
self.wgpu_renderer.update_direct_uniforms(
[x_min as f32, y_min as f32],
[x_max as f32, y_max as f32],
[ndc_left, ndc_bottom],
[ndc_right, ndc_top],
[sw, sh],
);
drop(render_pass);
let render_time = start_time.elapsed().as_secs_f64() * 1000.0;
Ok(RenderResult {
success: true,
data_bounds: self.data_bounds,
vertex_count: total_vertices,
triangle_count: total_triangles,
render_time_ms: render_time,
})
}
pub fn render(
&mut self,
encoder: &mut wgpu::CommandEncoder,
target: RenderTarget<'_>,
config: &PlotRenderConfig,
) -> Result<RenderResult, Box<dyn std::error::Error>> {
let start_time = Instant::now();
self.wgpu_renderer.ensure_msaa(config.msaa_samples);
let aspect_ratio = config.width as f32 / config.height as f32;
let mut cam = self.camera().clone();
cam.update_aspect_ratio(aspect_ratio);
let view_proj_matrix = cam.view_proj_matrix();
let model_matrix = Mat4::IDENTITY;
self.wgpu_renderer
.update_uniforms(view_proj_matrix, model_matrix);
let mut render_items = Vec::new();
let mut total_vertices = 0;
let mut total_triangles = 0;
for node in self.scene.get_visible_nodes() {
if let Some(render_data) = &node.render_data {
if let Some((vertex_buffer, index_buffer)) =
self.prepare_buffers_for_render_data(node.id, render_data)
{
self.wgpu_renderer
.ensure_pipeline(render_data.pipeline_type);
render_items.push((render_data, vertex_buffer, index_buffer));
total_vertices += render_data.vertex_count();
if let Some(indices) = &render_data.indices {
total_triangles += indices.len() / 3;
}
}
}
}
let mut image_bind_groups: Vec<Option<wgpu::BindGroup>> =
Vec::with_capacity(render_items.len());
let has_textured_items = render_items.iter().any(|(render_data, _, _)| {
render_data.pipeline_type == crate::core::PipelineType::Textured
});
if has_textured_items {
self.wgpu_renderer.ensure_image_pipeline();
let mut inferred_bounds: Option<(f64, f64, f64, f64)> = None;
for (render_data, _, _) in &render_items {
let Some(bounds) = render_data.bounds.as_ref() else {
continue;
};
let min_x = bounds.min.x as f64;
let max_x = bounds.max.x as f64;
let min_y = bounds.min.y as f64;
let max_y = bounds.max.y as f64;
inferred_bounds = Some(match inferred_bounds {
Some((x0, x1, y0, y1)) => {
(x0.min(min_x), x1.max(max_x), y0.min(min_y), y1.max(max_y))
}
None => (min_x, max_x, min_y, max_y),
});
}
let (mut x_min, mut x_max, mut y_min, mut y_max) = self
.data_bounds
.or(inferred_bounds)
.unwrap_or((-1.0, 1.0, -1.0, 1.0));
if (x_max - x_min).abs() < f64::EPSILON {
x_min -= 0.5;
x_max += 0.5;
}
if (y_max - y_min).abs() < f64::EPSILON {
y_min -= 0.5;
y_max += 0.5;
}
log::trace!(
target: "runmat_plot",
"direct uniforms bounds x=({}, {}) y=({}, {}) size=({}, {})",
x_min,
x_max,
y_min,
y_max,
config.width,
config.height
);
self.wgpu_renderer.update_direct_uniforms(
[x_min as f32, y_min as f32],
[x_max as f32, y_max as f32],
[-1.0, -1.0],
[1.0, 1.0],
[config.width as f32, config.height as f32],
);
}
for (render_data, _vb, _ib) in &render_items {
if render_data.pipeline_type == crate::core::PipelineType::Textured {
if let Some(crate::core::scene::ImageData::Rgba8 {
width,
height,
data,
}) = &render_data.image
{
let (_tex, _view, img_bg) = self
.wgpu_renderer
.create_image_texture_and_bind_group(*width, *height, data);
image_bind_groups.push(Some(img_bg));
} else {
image_bind_groups.push(None);
}
} else {
image_bind_groups.push(None);
}
}
let mut point_style_bind_groups: Vec<Option<wgpu::BindGroup>> =
Vec::with_capacity(render_items.len());
for (render_data, _vb, _ib) in &render_items {
if render_data.pipeline_type == crate::core::PipelineType::Points {
let style = crate::core::renderer::PointStyleUniforms {
face_color: render_data.material.albedo.to_array(),
edge_color: render_data.material.emissive.to_array(),
edge_thickness_px: render_data.material.roughness,
marker_shape: render_data.material.metallic as u32,
_pad: [0.0, 0.0],
};
let (_buf, bg) = self.wgpu_renderer.create_point_style_bind_group(style);
point_style_bind_groups.push(Some(bg));
} else {
point_style_bind_groups.push(None);
}
}
{
let depth_view = self.wgpu_renderer.ensure_depth_view();
let use_msaa = self.wgpu_renderer.msaa_sample_count > 1;
let mut cached_msaa_view: Option<Arc<wgpu::TextureView>> = None;
let (color_view, resolve_target) = if use_msaa {
if let Some(explicit_resolve_target) = target.resolve_target {
(target.view, Some(explicit_resolve_target))
} else {
cached_msaa_view = Some(self.wgpu_renderer.ensure_msaa_color_view());
(
cached_msaa_view
.as_ref()
.expect("msaa color view should exist")
.as_ref(),
Some(target.view),
)
}
} else {
(target.view, target.resolve_target)
};
let depth_clear = match self.wgpu_renderer.depth_mode {
crate::core::DepthMode::Standard => 1.0,
crate::core::DepthMode::ReversedZ => 0.0,
};
let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("Plot Render Pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: color_view,
resolve_target,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color {
r: config.background_color.x as f64,
g: config.background_color.y as f64,
b: config.background_color.z as f64,
a: config.background_color.w as f64,
}),
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
view: &depth_view,
depth_ops: Some(wgpu::Operations {
load: wgpu::LoadOp::Clear(depth_clear),
store: wgpu::StoreOp::Discard,
}),
stencil_ops: None,
}),
occlusion_query_set: None,
timestamp_writes: None,
});
let _keep_msaa_view_alive = &cached_msaa_view;
for (i, (render_data, vertex_buffer, index_buffer)) in render_items.iter().enumerate() {
#[cfg(target_arch = "wasm32")]
{
if log::log_enabled!(log::Level::Debug) {
if let Some(v0) = render_data.vertices.first() {
log::debug!(
target: "runmat_plot",
"wasm draw item: pipeline={:?} verts={} v0.pos=({:.3},{:.3},{:.3}) v0.color=({:.3},{:.3},{:.3},{:.3})",
render_data.pipeline_type,
render_data.vertices.len(),
v0.position[0],
v0.position[1],
v0.position[2],
v0.color[0],
v0.color[1],
v0.color[2],
v0.color[3],
);
} else if render_data.gpu_vertices.is_some() {
log::debug!(
target: "runmat_plot",
"wasm draw item: pipeline={:?} using gpu_vertices vertex_count={}",
render_data.pipeline_type,
render_data.vertex_count(),
);
} else {
log::debug!(
target: "runmat_plot",
"wasm draw item: pipeline={:?} has no vertices",
render_data.pipeline_type
);
}
}
}
if render_data.pipeline_type == crate::core::PipelineType::Textured {
let pipeline = self.wgpu_renderer.get_pipeline(render_data.pipeline_type);
render_pass.set_pipeline(pipeline);
render_pass.set_bind_group(
0,
&self.wgpu_renderer.direct_uniform_bind_group,
&[],
);
if let Some(ref img_bg) = image_bind_groups[i] {
render_pass.set_bind_group(1, img_bg, &[]);
}
} else {
let pipeline = self.wgpu_renderer.get_pipeline(render_data.pipeline_type);
render_pass.set_pipeline(pipeline);
render_pass.set_bind_group(0, self.wgpu_renderer.get_uniform_bind_group(), &[]);
}
render_pass.set_vertex_buffer(0, vertex_buffer.slice(..));
if let Some(index_buffer) = index_buffer {
render_pass.set_index_buffer(index_buffer.slice(..), wgpu::IndexFormat::Uint32);
if let Some(indices) = &render_data.indices {
log::trace!(target: "runmat_plot", "draw indexed count={}", indices.len());
render_pass.draw_indexed(0..indices.len() as u32, 0, 0..1);
}
} else {
log::trace!(target: "runmat_plot", "draw direct vertices");
if let Some((args, offset)) = Self::gpu_indirect_args(render_data) {
render_pass.draw_indirect(args, offset);
continue;
}
for draw_call in &render_data.draw_calls {
log::trace!(
target: "runmat_plot",
"draw vertices offset={} count={} instances={}",
draw_call.vertex_offset,
draw_call.vertex_count,
draw_call.instance_count
);
render_pass.draw(
draw_call.vertex_offset as u32
..(draw_call.vertex_offset + draw_call.vertex_count) as u32,
0..draw_call.instance_count as u32,
);
}
}
}
}
let render_time = start_time.elapsed().as_secs_f64() * 1000.0;
Ok(RenderResult {
success: true,
data_bounds: self.data_bounds,
vertex_count: total_vertices,
triangle_count: total_triangles,
render_time_ms: render_time,
})
}
pub fn render_scene_to_target(
&mut self,
encoder: &mut wgpu::CommandEncoder,
target_view: &wgpu::TextureView,
config: &PlotRenderConfig,
) -> Result<RenderResult, Box<dyn std::error::Error>> {
let start_time = Instant::now();
let (rows, cols) = self.figure_axes_grid();
let axes_count = rows.saturating_mul(cols);
log::debug!(
"runmat-plot: renderer.scene_to_target.start rows={} cols={} axes_count={} width={} height={}",
rows,
cols,
axes_count,
config.width,
config.height
);
if axes_count <= 1 {
log::debug!("runmat-plot: renderer.scene_to_target.branch_single_axes");
return self.render(
encoder,
RenderTarget {
view: target_view,
resolve_target: None,
},
config,
);
}
let viewports =
Self::compute_tiled_viewports(config.width.max(1), config.height.max(1), rows, cols);
log::debug!(
"runmat-plot: renderer.scene_to_target.branch_subplot_axes viewports={}",
viewports.len()
);
self.render_axes_to_viewports(
encoder,
target_view,
&viewports,
config.msaa_samples.max(1),
config,
)?;
let stats = self.scene.statistics();
Ok(RenderResult {
success: true,
data_bounds: self.data_bounds,
vertex_count: stats.total_vertices,
triangle_count: stats.total_triangles,
render_time_ms: start_time.elapsed().as_secs_f64() * 1000.0,
})
}
fn compute_tiled_viewports(
total_width: u32,
total_height: u32,
rows: usize,
cols: usize,
) -> Vec<(u32, u32, u32, u32)> {
if rows == 0 || cols == 0 {
return vec![(0, 0, total_width.max(1), total_height.max(1))];
}
let rows_u32 = rows as u32;
let cols_u32 = cols as u32;
let cell_w = (total_width / cols_u32).max(1);
let cell_h = (total_height / rows_u32).max(1);
let mut out = Vec::with_capacity(rows * cols);
for r in 0..rows_u32 {
for c in 0..cols_u32 {
let x = c * cell_w;
let y = r * cell_h;
let mut w = cell_w;
let mut h = cell_h;
if c + 1 == cols_u32 {
w = total_width.saturating_sub(x).max(1);
}
if r + 1 == rows_u32 {
h = total_height.saturating_sub(y).max(1);
}
out.push((x, y, w, h));
}
}
out
}
#[allow(clippy::too_many_arguments)]
pub fn render_camera_to_viewport(
&mut self,
encoder: &mut wgpu::CommandEncoder,
target_view: &wgpu::TextureView,
viewport_scissor: (u32, u32, u32, u32),
config: &PlotRenderConfig,
camera: &Camera,
axes_index: usize,
clear_background: bool,
) -> Result<RenderResult, Box<dyn std::error::Error>> {
log::debug!(
"runmat-plot: renderer.camera_to_viewport.start axes_index={} viewport=({}, {}, {}, {}) clear_background={}",
axes_index,
viewport_scissor.0,
viewport_scissor.1,
viewport_scissor.2,
viewport_scissor.3,
clear_background
);
let use_msaa = config.msaa_samples.max(1) > 1;
self.wgpu_renderer.ensure_msaa(config.msaa_samples);
let msaa_view_keepalive = if use_msaa {
Some(self.wgpu_renderer.ensure_msaa_color_view())
} else {
None
};
let render_target = if let Some(msaa_view) = msaa_view_keepalive.as_ref() {
RenderTarget {
view: msaa_view.as_ref(),
resolve_target: Some(target_view),
}
} else {
RenderTarget {
view: target_view,
resolve_target: None,
}
};
self.render_camera_to_target_viewport(
encoder,
render_target,
viewport_scissor,
config,
camera,
axes_index,
clear_background,
)
}
#[allow(clippy::too_many_arguments)]
fn render_camera_to_target_viewport(
&mut self,
encoder: &mut wgpu::CommandEncoder,
target: RenderTarget<'_>,
viewport_scissor: (u32, u32, u32, u32),
config: &PlotRenderConfig,
camera: &Camera,
axes_index: usize,
clear_background: bool,
) -> Result<RenderResult, Box<dyn std::error::Error>> {
let start_time = Instant::now();
self.wgpu_renderer.ensure_msaa(config.msaa_samples);
self.wgpu_renderer.set_depth_mode(config.depth_mode);
let depth_view = self.wgpu_renderer.ensure_depth_view();
let aspect_ratio = (config.width.max(1)) as f32 / (config.height.max(1)) as f32;
let mut cam = camera.clone();
cam.update_aspect_ratio(aspect_ratio);
cam.depth_mode = config.depth_mode;
log::debug!(
"runmat-plot: renderer.camera_to_target_viewport.camera_ready axes_index={} aspect_ratio={} msaa_samples={}",
axes_index,
aspect_ratio,
config.msaa_samples
);
if config.clip_policy.dynamic {
let mut bounds: Option<crate::core::scene::BoundingBox> = None;
for node in self.scene.get_visible_nodes() {
if let Some(rd) = &node.render_data {
if let Some(b) = rd.bounds {
bounds = Some(bounds.map_or(b, |acc| acc.union(&b)));
}
}
}
if let Some(b) = bounds {
cam.update_clip_planes_from_world_aabb(b.min, b.max, &config.clip_policy);
}
}
let view_proj_matrix = cam.view_proj_matrix();
self.wgpu_renderer
.update_uniforms_for_axes(axes_index, view_proj_matrix, Mat4::IDENTITY);
log::debug!(
"runmat-plot: renderer.camera_to_target_viewport.uniforms_updated axes_index={}",
axes_index
);
let (mut sx, mut sy, mut sw, mut sh) = viewport_scissor;
let target_w = self.wgpu_renderer.surface_config.width.max(1);
let target_h = self.wgpu_renderer.surface_config.height.max(1);
if sx >= target_w || sy >= target_h {
return Ok(RenderResult {
success: true,
data_bounds: self.data_bounds,
vertex_count: 0,
triangle_count: 0,
render_time_ms: 0.0,
});
}
sx = sx.min(target_w.saturating_sub(1));
sy = sy.min(target_h.saturating_sub(1));
sw = sw.max(1).min(target_w.saturating_sub(sx).max(1));
sh = sh.max(1).min(target_h.saturating_sub(sy).max(1));
let is_2d = matches!(
cam.projection,
crate::core::camera::ProjectionType::Orthographic { .. }
);
log::debug!(
"runmat-plot: renderer.camera_to_target_viewport.viewport_normalized axes_index={} viewport=({}, {}, {}, {}) is_2d={}",
axes_index,
sx,
sy,
sw,
sh,
is_2d
);
match cam.projection {
crate::core::camera::ProjectionType::Orthographic {
left,
right,
bottom,
top,
..
} => {
log::debug!(
target: "runmat_plot.draw_camera",
"draw camera axes_index={} is_2d=true viewport=({}, {}, {}, {}) bounds=({}, {})..({}, {}) cfg_wh=({}, {})",
axes_index,
sx,
sy,
sw,
sh,
left,
bottom,
right,
top,
config.width,
config.height
);
}
crate::core::camera::ProjectionType::Perspective { .. } => {
log::debug!(
target: "runmat_plot.draw_camera",
"draw camera axes_index={} is_2d=false viewport=({}, {}, {}, {}) cfg_wh=({}, {})",
axes_index,
sx,
sy,
sw,
sh,
config.width,
config.height
);
}
}
let mut owned_render_data: Vec<Box<crate::core::RenderData>> = Vec::new();
let mut render_items = Vec::new();
let mut grid_plane_buffers: Option<(wgpu::Buffer, wgpu::Buffer)> = None;
let mut total_vertices = 0usize;
let mut total_triangles = 0usize;
log::debug!(
"runmat-plot: renderer.camera_to_target_viewport.collect_render_items.start axes_index={}",
axes_index
);
for node in self.scene.get_visible_nodes() {
if let Some(render_data) = &node.render_data {
if node.axes_index == axes_index {
log::debug!(
target: "runmat_plot.draw_item",
"draw item axes_index={} node_axes_index={} pipeline={:?} vertex_count={} has_indices={} has_bounds={} gpu_vertices={}",
axes_index,
node.axes_index,
render_data.pipeline_type,
render_data.vertex_count(),
render_data.indices.is_some(),
render_data.bounds.is_some(),
render_data.gpu_vertices.is_some()
);
}
if let Some((vb, ib)) = self.prepare_buffers_for_render_data(node.id, render_data) {
self.wgpu_renderer
.ensure_pipeline(render_data.pipeline_type);
total_vertices += render_data.vertex_count();
if let Some(indices) = &render_data.indices {
total_triangles += indices.len() / 3;
}
render_items.push((render_data, vb, ib));
}
}
}
log::debug!(
"runmat-plot: renderer.camera_to_target_viewport.collect_render_items.ok axes_index={} items={} total_vertices={} total_triangles={}",
axes_index,
render_items.len(),
total_vertices,
total_triangles
);
if !is_2d {
let view_proj = view_proj_matrix;
let inv_view_proj = view_proj.inverse();
let unproject = |ndc_x: f32, ndc_y: f32, ndc_z: f32| -> Option<Vec3> {
let clip = Vec4::new(ndc_x, ndc_y, ndc_z, 1.0);
let world = inv_view_proj * clip;
if !world.w.is_finite() || world.w.abs() < 1e-6 {
return None;
}
let p = world.truncate() / world.w;
if p.x.is_finite() && p.y.is_finite() && p.z.is_finite() {
Some(p)
} else {
None
}
};
let ray_intersect_z0 = |ndc_x: f32, ndc_y: f32| -> Option<Vec3> {
let p0 = unproject(ndc_x, ndc_y, -1.0)?;
let p1 = unproject(ndc_x, ndc_y, 1.0)?;
let dir = p1 - p0;
if !dir.z.is_finite() || dir.z.abs() < 1e-8 {
return None;
}
let t = (-p0.z) / dir.z;
if !t.is_finite() || t <= 0.0 {
return None;
}
Some(p0 + dir * t)
};
let mut plane_pts: Vec<Vec3> = Vec::new();
for (nx, ny) in [(-1.0, -1.0), (1.0, -1.0), (1.0, 1.0), (-1.0, 1.0)] {
if let Some(p) = ray_intersect_z0(nx, ny) {
plane_pts.push(p);
}
}
let mut min_x = 0.0_f32;
let mut max_x = 1.0_f32;
let mut min_y = 0.0_f32;
let mut max_y = 1.0_f32;
if plane_pts.len() >= 2 {
min_x = plane_pts.iter().map(|p| p.x).fold(f32::INFINITY, f32::min);
max_x = plane_pts
.iter()
.map(|p| p.x)
.fold(f32::NEG_INFINITY, f32::max);
min_y = plane_pts.iter().map(|p| p.y).fold(f32::INFINITY, f32::min);
max_y = plane_pts
.iter()
.map(|p| p.y)
.fold(f32::NEG_INFINITY, f32::max);
} else if let crate::core::camera::ProjectionType::Perspective { fov, .. } =
cam.projection
{
let dist = (cam.position - cam.target).length().max(1e-3);
let extent = (dist * (0.5 * fov).tan() * 1.25).max(0.5);
let center = Vec3::new(cam.target.x, cam.target.y, 0.0);
min_x = center.x - extent;
max_x = center.x + extent;
min_y = center.y - extent;
max_y = center.y + extent;
}
let dx = (max_x - min_x).abs().max(1e-3);
let dy = (max_y - min_y).abs().max(1e-3);
let margin_x = dx * 0.04;
let margin_y = dy * 0.04;
min_x -= margin_x;
max_x += margin_x;
min_y -= margin_y;
max_y += margin_y;
let project_to_px = |p: Vec3| -> Option<(f32, f32)> {
let clip = view_proj * Vec4::new(p.x, p.y, p.z, 1.0);
if !clip.w.is_finite() || clip.w.abs() < 1e-6 {
return None;
}
let ndc = clip.truncate() / clip.w;
if !(ndc.x.is_finite() && ndc.y.is_finite()) {
return None;
}
let px = ((ndc.x + 1.0) * 0.5) * (sw.max(1) as f32);
let py = ((1.0 - ndc.y) * 0.5) * (sh.max(1) as f32);
Some((px, py))
};
let nice_step = |raw: f64| -> f64 {
if !raw.is_finite() || raw <= 0.0 {
return 1.0;
}
let pow10 = 10.0_f64.powf(raw.log10().floor());
let norm = raw / pow10;
let mult = if norm <= 1.0 {
1.0
} else if norm <= 2.0 {
2.0
} else if norm <= 5.0 {
5.0
} else {
10.0
};
mult * pow10
};
let cx = (min_x + max_x) * 0.5;
let cy = (min_y + max_y) * 0.5;
let center = Vec3::new(cx, cy, 0.0);
let px_per_world = {
let a = project_to_px(center);
let b = project_to_px(center + Vec3::new(1.0, 0.0, 0.0));
match (a, b) {
(Some((ax, ay)), Some((bx, by))) => ((bx - ax).hypot(by - ay)).max(1e-3),
_ => 1.0,
}
};
let desired_major_px = 120.0_f64;
let major_step = nice_step((desired_major_px / (px_per_world as f64)).max(1e-6));
let mut minor_step = major_step / 10.0;
if !minor_step.is_finite() || minor_step <= 0.0 {
minor_step = major_step.max(1.0);
}
let max_minor_lines = 180.0;
let minor_count_x = (dx as f64 / minor_step).abs();
let minor_count_y = (dy as f64 / minor_step).abs();
if minor_count_x > max_minor_lines || minor_count_y > max_minor_lines {
minor_step = (major_step / 5.0).max(major_step); }
let mut helper_vertices: Vec<Vertex> = Vec::new();
let mut push_line = |a: Vec3, b: Vec3, color: Vec4| {
helper_vertices.push(Vertex::new(a, color));
helper_vertices.push(Vertex::new(b, color));
};
let z_grid = -1e-4_f32;
if self.overlay_show_grid_for_axes(axes_index) {
let theme = self.theme.build_theme();
let bg = theme.get_background_color();
let grid = theme.get_grid_color();
let bg_luma = 0.2126 * bg.x + 0.7152 * bg.y + 0.0722 * bg.z;
let mut major_rgb = [grid.x, grid.y, grid.z];
let mut minor_rgb = [grid.x, grid.y, grid.z];
let mut major_alpha = grid.w.clamp(0.08, 0.22);
let mut minor_alpha = (grid.w * 0.45).clamp(0.04, 0.14);
if bg_luma <= 0.62 {
major_rgb = [grid.x * 0.80, grid.y * 0.80, grid.z * 0.80];
minor_rgb = [grid.x * 0.68, grid.y * 0.68, grid.z * 0.68];
}
if bg_luma > 0.62 {
major_rgb = [grid.x * 0.45, grid.y * 0.45, grid.z * 0.45];
minor_rgb = [grid.x * 0.33, grid.y * 0.33, grid.z * 0.33];
major_alpha = major_alpha.max(0.24);
minor_alpha = minor_alpha.max(0.12);
}
self.wgpu_renderer.ensure_grid_plane_pipeline();
self.wgpu_renderer.update_grid_uniforms_for_axes(
axes_index,
crate::core::renderer::GridUniforms {
major_step: major_step as f32,
minor_step: minor_step as f32,
fade_start: (0.60 * dx.max(dy)).max(major_step as f32),
fade_end: (0.95 * dx.max(dy)).max((major_step as f32) * 2.0),
camera_pos: cam.position.to_array(),
_pad0: 0.0,
target_pos: Vec3::new(cam.target.x, cam.target.y, 0.0).to_array(),
_pad1: 0.0,
major_color: [major_rgb[0], major_rgb[1], major_rgb[2], major_alpha],
minor_color: [minor_rgb[0], minor_rgb[1], minor_rgb[2], minor_alpha],
},
);
let quad_vertices = [
Vertex::new(Vec3::new(min_x, min_y, z_grid), Vec4::ONE),
Vertex::new(Vec3::new(max_x, min_y, z_grid), Vec4::ONE),
Vertex::new(Vec3::new(max_x, max_y, z_grid), Vec4::ONE),
Vertex::new(Vec3::new(min_x, max_y, z_grid), Vec4::ONE),
];
let quad_indices: [u32; 6] = [0, 1, 2, 0, 2, 3];
let vb = self.wgpu_renderer.create_vertex_buffer(&quad_vertices);
let ib = self.wgpu_renderer.create_index_buffer(&quad_indices);
grid_plane_buffers = Some((vb, ib));
}
let axis_len = (major_step as f32 * 5.0).clamp(0.5, (dx.max(dy) * 0.6).max(0.5));
let origin = Vec3::new(0.0, 0.0, 0.0);
let col_x = Vec4::new(0.92, 0.25, 0.25, 0.85);
let col_y = Vec4::new(0.35, 0.90, 0.45, 0.85);
let col_z = Vec4::new(0.35, 0.62, 0.98, 0.85);
push_line(origin, origin + Vec3::new(axis_len, 0.0, 0.0), col_x);
push_line(origin, origin + Vec3::new(0.0, axis_len, 0.0), col_y);
push_line(origin, origin + Vec3::new(0.0, 0.0, axis_len), col_z);
let tick_max = (major_step as f32 * 0.25).max(1.0e-6);
let tick_min = 0.01_f32.min(tick_max);
let tick_len = (axis_len * 0.04).clamp(tick_min, tick_max);
let max_ticks = 6usize;
let mut add_ticks = |axis: Vec3, perp: Vec3, col: Vec4| {
if major_step <= 0.0 {
return;
}
for i in 1..=max_ticks {
let t = (i as f32) * (major_step as f32);
if t >= axis_len * 0.999 {
break;
}
let p = origin + axis * t;
push_line(
p - perp * tick_len,
p + perp * tick_len,
Vec4::new(col.x, col.y, col.z, col.w * 0.85),
);
}
};
add_ticks(Vec3::X, Vec3::Y, col_x);
add_ticks(Vec3::Y, Vec3::X, col_y);
add_ticks(Vec3::Z, Vec3::X, col_z);
if !helper_vertices.is_empty() {
let rd = Box::new(crate::core::RenderData {
pipeline_type: crate::core::PipelineType::Lines,
vertices: helper_vertices,
indices: None,
gpu_vertices: None,
bounds: None,
material: crate::core::Material::default(),
draw_calls: vec![crate::core::DrawCall {
vertex_offset: 0,
vertex_count: 0, index_offset: None,
index_count: None,
instance_count: 1,
}],
image: None,
});
owned_render_data.push(rd);
let idx = owned_render_data.len() - 1;
let vcount = owned_render_data[idx].vertices.len();
if let Some(dc) = owned_render_data[idx].draw_calls.get_mut(0) {
dc.vertex_count = vcount;
}
let vb = Arc::new(
self.wgpu_renderer
.create_vertex_buffer(&owned_render_data[idx].vertices),
);
let rd_ref: &crate::core::RenderData = &owned_render_data[idx];
render_items.insert(0, (rd_ref, vb, None));
total_vertices += vcount;
}
}
let mut point_buffers: Vec<Option<(wgpu::Buffer, usize)>> =
Vec::with_capacity(render_items.len());
for (render_data, _vb, _ib) in render_items.iter() {
if matches!(render_data.pipeline_type, crate::core::PipelineType::Points) {
let expanded = self
.wgpu_renderer
.create_direct_point_vertices(&render_data.vertices, 0.0);
let buf = self.wgpu_renderer.create_vertex_buffer(&expanded);
point_buffers.push(Some((buf, expanded.len())));
} else {
point_buffers.push(None);
}
}
let has_textured_items = render_items.iter().any(|(render_data, _vb, _ib)| {
render_data.pipeline_type == crate::core::PipelineType::Textured
});
if has_textured_items {
self.wgpu_renderer.ensure_image_pipeline();
}
let mut image_bind_groups: Vec<Option<wgpu::BindGroup>> =
Vec::with_capacity(render_items.len());
for (render_data, _vb, _ib) in render_items.iter() {
if render_data.pipeline_type == crate::core::PipelineType::Textured {
if let Some(crate::core::scene::ImageData::Rgba8 {
width,
height,
data,
}) = &render_data.image
{
let (_t, _v, bg) = self
.wgpu_renderer
.create_image_texture_and_bind_group(*width, *height, data);
image_bind_groups.push(Some(bg));
} else {
image_bind_groups.push(None);
}
} else {
image_bind_groups.push(None);
}
}
let mut point_style_bind_groups: Vec<Option<wgpu::BindGroup>> =
Vec::with_capacity(render_items.len());
for (render_data, _vb, _ib) in render_items.iter() {
if matches!(render_data.pipeline_type, crate::core::PipelineType::Points) {
let style = crate::core::renderer::PointStyleUniforms {
face_color: render_data.material.albedo.to_array(),
edge_color: render_data.material.emissive.to_array(),
edge_thickness_px: render_data.material.roughness,
marker_shape: render_data.material.metallic as u32,
_pad: [0.0, 0.0],
};
let (_buf, bg) = self.wgpu_renderer.create_point_style_bind_group(style);
point_style_bind_groups.push(Some(bg));
} else {
point_style_bind_groups.push(None);
}
}
let mut grid_vb_opt: Option<wgpu::Buffer> = None;
if is_2d && self.overlay_show_grid_for_axes(axes_index) {
if let Some((l, r, b, t)) = self.view_bounds_for_axes(axes_index) {
self.wgpu_renderer.update_direct_uniforms_for_axes(
axes_index,
[l as f32, b as f32],
[r as f32, t as f32],
[-1.0, -1.0],
[1.0, 1.0],
[sw.max(1) as f32, sh.max(1) as f32],
);
self.wgpu_renderer.ensure_direct_line_pipeline();
let x_range = (r - l).max(1e-6);
let y_range = (t - b).max(1e-6);
let x_step = plot_utils::calculate_tick_interval(x_range);
let y_step = plot_utils::calculate_tick_interval(y_range);
let mut grid_vertices: Vec<Vertex> = Vec::new();
let g = 80.0_f32 / 255.0_f32;
let col = Vec4::new(g, g, g, 1.0);
if x_step.is_finite() && x_step > 0.0 {
let mut x = ((l / x_step).ceil() * x_step) as f32;
let b_f = b as f32;
let t_f = t as f32;
while (x as f64) <= r {
grid_vertices.push(Vertex::new(Vec3::new(x, b_f, 0.0), col));
grid_vertices.push(Vertex::new(Vec3::new(x, t_f, 0.0), col));
x += x_step as f32;
}
}
if y_step.is_finite() && y_step > 0.0 {
let mut y = ((b / y_step).ceil() * y_step) as f32;
let l_f = l as f32;
let r_f = r as f32;
while (y as f64) <= t {
grid_vertices.push(Vertex::new(Vec3::new(l_f, y, 0.0), col));
grid_vertices.push(Vertex::new(Vec3::new(r_f, y, 0.0), col));
y += y_step as f32;
}
}
if !grid_vertices.is_empty() {
grid_vb_opt = Some(self.wgpu_renderer.create_vertex_buffer(&grid_vertices));
}
}
}
let bounds_opt = if is_2d {
match cam.projection {
crate::core::camera::ProjectionType::Orthographic {
left,
right,
bottom,
top,
..
} => Some((left as f64, right as f64, bottom as f64, top as f64)),
_ => self.data_bounds,
}
} else {
None
};
if is_2d {
if let Some((l, r, b, t)) = bounds_opt {
self.wgpu_renderer.update_direct_uniforms_for_axes(
axes_index,
[l as f32, b as f32],
[r as f32, t as f32],
[-1.0, -1.0],
[1.0, 1.0],
[sw.max(1) as f32, sh.max(1) as f32],
);
}
self.wgpu_renderer.ensure_direct_triangle_pipeline();
self.wgpu_renderer.ensure_direct_line_pipeline();
self.wgpu_renderer.ensure_direct_point_pipeline();
} else {
self.wgpu_renderer
.ensure_pipeline(crate::core::PipelineType::Triangles);
self.wgpu_renderer
.ensure_pipeline(crate::core::PipelineType::Lines);
self.wgpu_renderer
.ensure_pipeline(crate::core::PipelineType::Points);
}
{
let use_msaa = self.wgpu_renderer.msaa_sample_count > 1;
log::debug!(
"runmat-plot: renderer.camera_to_target_viewport.render_pass_start axes_index={} use_msaa={} clear_background={}",
axes_index,
use_msaa,
clear_background
);
let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("Plot Camera Viewport Pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: target.view,
resolve_target: if use_msaa {
target.resolve_target
} else {
None
},
ops: wgpu::Operations {
load: if clear_background {
wgpu::LoadOp::Clear(wgpu::Color {
r: config.background_color.x as f64,
g: config.background_color.y as f64,
b: config.background_color.z as f64,
a: config.background_color.w as f64,
})
} else {
wgpu::LoadOp::Load
},
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
view: depth_view.as_ref(),
depth_ops: Some(wgpu::Operations {
load: wgpu::LoadOp::Clear(match config.depth_mode {
DepthMode::Standard => 1.0,
DepthMode::ReversedZ => 0.0,
}),
store: wgpu::StoreOp::Store,
}),
stencil_ops: None,
}),
timestamp_writes: None,
occlusion_query_set: None,
});
render_pass.set_viewport(
sx as f32,
sy as f32,
sw.max(1) as f32,
sh.max(1) as f32,
0.0,
1.0,
);
render_pass.set_scissor_rect(sx, sy, sw.max(1), sh.max(1));
log::debug!(
"runmat-plot: renderer.camera_to_target_viewport.render_pass_ready axes_index={} viewport=({}, {}, {}, {})",
axes_index,
sx,
sy,
sw.max(1),
sh.max(1)
);
if let Some(ref vb_grid) = grid_vb_opt {
if let Some(ref pipeline) = self.wgpu_renderer.direct_line_pipeline {
log::debug!(
"runmat-plot: renderer.camera_to_target_viewport.draw_grid_start axes_index={} vertex_buffer_size={}",
axes_index,
vb_grid.size()
);
render_pass.set_pipeline(pipeline);
render_pass.set_bind_group(
0,
self.wgpu_renderer
.get_direct_uniform_bind_group_for_axes(axes_index),
&[],
);
render_pass.set_vertex_buffer(0, vb_grid.slice(..));
render_pass.draw(
0..(vb_grid.size() / std::mem::size_of::<Vertex>() as u64) as u32,
0..1,
);
log::debug!(
"runmat-plot: renderer.camera_to_target_viewport.draw_grid_ok axes_index={}",
axes_index
);
}
}
let use_direct_for_triangles = is_2d;
let use_direct_for_lines = is_2d;
let direct_tri_pipeline = if use_direct_for_triangles && bounds_opt.is_some() {
self.wgpu_renderer
.direct_triangle_pipeline
.as_ref()
.map(|p| p as *const wgpu::RenderPipeline)
} else {
None
};
let direct_line_pipeline = if use_direct_for_lines && bounds_opt.is_some() {
self.wgpu_renderer
.direct_line_pipeline
.as_ref()
.map(|p| p as *const wgpu::RenderPipeline)
} else {
None
};
let direct_point_pipeline = if is_2d && bounds_opt.is_some() {
self.wgpu_renderer
.direct_point_pipeline
.as_ref()
.map(|p| p as *const wgpu::RenderPipeline)
} else {
None
};
let mut __temp_point_buffers_cam: Vec<wgpu::Buffer> = Vec::new();
for (idx, (render_data, vertex_buffer, index_buffer)) in render_items.iter().enumerate()
{
let is_triangles = matches!(
render_data.pipeline_type,
crate::core::PipelineType::Triangles
);
let is_lines =
matches!(render_data.pipeline_type, crate::core::PipelineType::Lines);
let is_points =
matches!(render_data.pipeline_type, crate::core::PipelineType::Points);
let is_textured = matches!(
render_data.pipeline_type,
crate::core::PipelineType::Textured
);
let use_direct = is_2d
&& ((use_direct_for_triangles && is_triangles)
|| (use_direct_for_lines && is_lines)
|| is_points)
&& bounds_opt.is_some();
log::debug!(
"runmat-plot: renderer.camera_to_target_viewport.draw_item_start axes_index={} item_index={} pipeline={:?} use_direct={} textured={} indexed={} draw_calls={} point_buffer={} ",
axes_index,
idx,
render_data.pipeline_type,
use_direct,
is_textured,
index_buffer.is_some(),
render_data.draw_calls.len(),
point_buffers[idx].is_some()
);
if use_direct {
let pipeline_ref: &wgpu::RenderPipeline = unsafe {
if is_triangles {
direct_tri_pipeline.unwrap().as_ref().unwrap()
} else if is_lines {
direct_line_pipeline.unwrap().as_ref().unwrap()
} else {
direct_point_pipeline.unwrap().as_ref().unwrap()
}
};
let uniform_bg = self
.wgpu_renderer
.get_direct_uniform_bind_group_for_axes(axes_index);
render_pass.set_pipeline(pipeline_ref);
render_pass.set_bind_group(0, uniform_bg, &[]);
log::debug!(
"runmat-plot: renderer.camera_to_target_viewport.draw_item_pipeline_ready axes_index={} item_index={} branch=direct",
axes_index,
idx
);
} else if is_textured {
let pipeline = self
.wgpu_renderer
.get_pipeline(crate::core::PipelineType::Textured);
render_pass.set_pipeline(pipeline);
render_pass.set_bind_group(
0,
self.wgpu_renderer
.get_direct_uniform_bind_group_for_axes(axes_index),
&[],
);
if let Some(ref bg) = image_bind_groups[idx] {
render_pass.set_bind_group(1, bg, &[]);
}
log::debug!(
"runmat-plot: renderer.camera_to_target_viewport.draw_item_pipeline_ready axes_index={} item_index={} branch=textured",
axes_index,
idx
);
} else {
let pipeline = self.wgpu_renderer.get_pipeline(render_data.pipeline_type);
render_pass.set_pipeline(pipeline);
render_pass.set_bind_group(
0,
self.wgpu_renderer
.get_uniform_bind_group_for_axes(axes_index),
&[],
);
log::debug!(
"runmat-plot: renderer.camera_to_target_viewport.draw_item_pipeline_ready axes_index={} item_index={} branch=standard",
axes_index,
idx
);
}
if is_points && use_direct {
if let Some((ref buf, len)) = point_buffers[idx] {
if let Some(ref bg) = point_style_bind_groups[idx] {
render_pass.set_bind_group(1, bg, &[]);
}
render_pass.set_vertex_buffer(0, buf.slice(..));
render_pass.draw(0..len as u32, 0..1);
log::debug!(
"runmat-plot: renderer.camera_to_target_viewport.draw_item_ok axes_index={} item_index={} mode=direct_points vertices={}",
axes_index,
idx,
len
);
continue;
}
} else {
render_pass.set_vertex_buffer(0, vertex_buffer.slice(..));
}
if let Some(index_buffer_ref) = index_buffer {
render_pass
.set_index_buffer(index_buffer_ref.slice(..), wgpu::IndexFormat::Uint32);
if let Some(indices) = &render_data.indices {
render_pass.draw_indexed(0..indices.len() as u32, 0, 0..1);
log::debug!(
"runmat-plot: renderer.camera_to_target_viewport.draw_item_ok axes_index={} item_index={} mode=indexed indices={}",
axes_index,
idx,
indices.len()
);
}
} else {
for dc in &render_data.draw_calls {
render_pass.draw(
dc.vertex_offset as u32..(dc.vertex_offset + dc.vertex_count) as u32,
0..dc.instance_count as u32,
);
log::debug!(
"runmat-plot: renderer.camera_to_target_viewport.draw_call_ok axes_index={} item_index={} mode=draw vertex_offset={} vertex_count={} instances={}",
axes_index,
idx,
dc.vertex_offset,
dc.vertex_count,
dc.instance_count
);
}
}
}
if let Some((ref vb, ref ib)) = grid_plane_buffers {
if let Some(pipeline) = self.wgpu_renderer.grid_plane_pipeline() {
log::debug!(
"runmat-plot: renderer.camera_to_target_viewport.draw_grid_plane_start axes_index={}",
axes_index
);
render_pass.set_pipeline(pipeline);
render_pass.set_bind_group(
0,
self.wgpu_renderer
.get_uniform_bind_group_for_axes(axes_index),
&[],
);
render_pass.set_bind_group(
1,
self.wgpu_renderer
.get_grid_uniform_bind_group_for_axes(axes_index),
&[],
);
render_pass.set_vertex_buffer(0, vb.slice(..));
render_pass.set_index_buffer(ib.slice(..), wgpu::IndexFormat::Uint32);
render_pass.draw_indexed(0..6, 0, 0..1);
log::debug!(
"runmat-plot: renderer.camera_to_target_viewport.draw_grid_plane_ok axes_index={}",
axes_index
);
}
}
}
log::debug!(
"runmat-plot: renderer.camera_to_target_viewport.ok axes_index={} total_vertices={} total_triangles={}",
axes_index,
total_vertices,
total_triangles
);
Ok(RenderResult {
success: true,
data_bounds: self.data_bounds,
vertex_count: total_vertices,
triangle_count: total_triangles,
render_time_ms: start_time.elapsed().as_secs_f64() * 1000.0,
})
}
pub fn render_axes_to_viewports(
&mut self,
encoder: &mut wgpu::CommandEncoder,
target_view: &wgpu::TextureView,
axes_viewports: &[(u32, u32, u32, u32)],
msaa_samples: u32,
base_config: &PlotRenderConfig,
) -> Result<(), Box<dyn std::error::Error>> {
log::debug!(
"runmat-plot: renderer.axes_to_viewports.start viewport_count={} msaa_samples={} width={} height={}",
axes_viewports.len(),
msaa_samples,
base_config.width,
base_config.height
);
let mut axes_to_nodes: std::collections::HashMap<usize, Vec<crate::core::scene::NodeId>> =
std::collections::HashMap::new();
for node in self.scene.get_visible_nodes() {
axes_to_nodes
.entry(node.axes_index)
.or_default()
.push(node.id);
}
if self.axes_cameras.is_empty() {
self.axes_cameras.push(Self::create_default_camera());
}
self.wgpu_renderer
.ensure_axes_uniform_capacity(axes_viewports.len().max(1));
let all_ids: Vec<crate::core::scene::NodeId> = self
.scene
.get_visible_nodes()
.into_iter()
.map(|n| n.id)
.collect();
let active_axes: Vec<usize> = axes_viewports
.iter()
.enumerate()
.filter_map(|(ax_idx, _)| {
axes_to_nodes
.get(&ax_idx)
.filter(|ids| !ids.is_empty())
.map(|_| ax_idx)
})
.collect();
if active_axes.is_empty() {
log::debug!("runmat-plot: renderer.axes_to_viewports.no_active_axes");
return Ok(());
}
self.wgpu_renderer.ensure_msaa(msaa_samples.max(1));
let shared_msaa_view = if self.wgpu_renderer.msaa_sample_count > 1 {
Some(self.wgpu_renderer.ensure_msaa_color_view())
} else {
None
};
for (ax_idx, viewport) in axes_viewports.iter().enumerate() {
log::debug!(
"runmat-plot: renderer.axes_to_viewports.viewport axes_index={} viewport=({}, {}, {}, {})",
ax_idx,
viewport.0,
viewport.1,
viewport.2,
viewport.3
);
let ids_for_axes = axes_to_nodes.get(&ax_idx).cloned().unwrap_or_default();
if ids_for_axes.is_empty() {
log::debug!(
"runmat-plot: renderer.axes_to_viewports.skip_empty_axes axes_index={}",
ax_idx
);
continue;
}
let mut hidden_ids: Vec<crate::core::scene::NodeId> = Vec::new();
for id in &all_ids {
if !ids_for_axes.contains(id) {
if let Some(node) = self.scene.get_node_mut(*id) {
if node.visible {
node.visible = false;
hidden_ids.push(*id);
}
}
}
}
let cam = self
.axes_cameras
.get(ax_idx)
.cloned()
.unwrap_or_else(Self::create_default_camera);
let _ = self.calculate_data_bounds();
let mut cfg = base_config.clone();
cfg.width = viewport.2;
cfg.height = viewport.3;
cfg.msaa_samples = msaa_samples.max(1);
let is_first_axes = Some(&ax_idx) == active_axes.first();
let is_last_axes = Some(&ax_idx) == active_axes.last();
log::debug!(
"runmat-plot: renderer.axes_to_viewports.axes_ready axes_index={} node_count={} first_axes={} last_axes={}",
ax_idx,
ids_for_axes.len(),
is_first_axes,
is_last_axes
);
let render_target = if let Some(ref msaa_view) = shared_msaa_view {
RenderTarget {
view: msaa_view.as_ref(),
resolve_target: if is_last_axes {
Some(target_view)
} else {
None
},
}
} else {
RenderTarget {
view: target_view,
resolve_target: None,
}
};
let _ = self.render_camera_to_target_viewport(
encoder,
render_target,
*viewport,
&cfg,
&cam,
ax_idx,
is_first_axes,
)?;
log::debug!(
"runmat-plot: renderer.axes_to_viewports.axes_render_ok axes_index={}",
ax_idx
);
for id in hidden_ids {
if let Some(node) = self.scene.get_node_mut(id) {
node.visible = true;
}
}
}
log::debug!("runmat-plot: renderer.axes_to_viewports.ok");
Ok(())
}
fn create_default_camera() -> Camera {
let mut camera = Camera::new();
camera.projection = crate::core::camera::ProjectionType::Orthographic {
left: -5.0,
right: 5.0,
bottom: -5.0,
top: 5.0,
near: -10.0,
far: 10.0,
};
camera.depth_mode = DepthMode::default();
camera.position = Vec3::new(0.0, 0.0, 1.0);
camera.target = Vec3::new(0.0, 0.0, 0.0);
camera.up = Vec3::new(0.0, 1.0, 0.0);
camera
}
pub fn camera(&self) -> &Camera {
self.axes_cameras
.first()
.expect("axes_cameras must contain at least one camera")
}
pub fn camera_mut(&mut self) -> &mut Camera {
self.axes_cameras
.first_mut()
.expect("axes_cameras must contain at least one camera")
}
pub fn axes_camera(&self, axes_index: usize) -> Option<&Camera> {
self.axes_cameras.get(axes_index)
}
pub fn scene(&self) -> &Scene {
&self.scene
}
pub fn scene_statistics(&self) -> crate::core::SceneStatistics {
self.scene.statistics()
}
pub fn view_bounds(&self) -> Option<(f64, f64, f64, f64)> {
match self.camera().projection {
crate::core::camera::ProjectionType::Orthographic {
left,
right,
bottom,
top,
..
} => Some((left as f64, right as f64, bottom as f64, top as f64)),
_ => None,
}
}
pub fn overlay_show_grid(&self) -> bool {
self.figure_show_grid
}
pub fn overlay_show_grid_for_axes(&self, axes_index: usize) -> bool {
self.last_figure
.as_ref()
.and_then(|f| f.axes_metadata(axes_index))
.map(|m| m.grid_enabled)
.unwrap_or(self.figure_show_grid)
}
pub fn overlay_show_box(&self) -> bool {
self.figure_show_box
}
pub fn overlay_show_box_for_axes(&self, axes_index: usize) -> bool {
self.last_figure
.as_ref()
.and_then(|f| f.axes_metadata(axes_index))
.map(|m| m.box_enabled)
.unwrap_or(self.figure_show_box)
}
pub fn overlay_title(&self) -> Option<&String> {
self.figure_title.as_ref()
}
pub fn overlay_title_for_axes(&self, axes_index: usize) -> Option<&String> {
self.last_figure
.as_ref()
.and_then(|f| f.axes_metadata(axes_index))
.and_then(|m| m.title.as_ref())
}
pub fn overlay_x_label(&self) -> Option<&String> {
self.figure_x_label.as_ref()
}
pub fn overlay_x_label_for_axes(&self, axes_index: usize) -> Option<&String> {
self.last_figure
.as_ref()
.and_then(|f| f.axes_metadata(axes_index))
.and_then(|m| m.x_label.as_ref())
}
pub fn overlay_x_label_style_for_axes(&self, axes_index: usize) -> Option<&TextStyle> {
self.last_figure
.as_ref()
.and_then(|f| f.axes_metadata(axes_index))
.map(|m| &m.x_label_style)
}
pub fn overlay_y_label(&self) -> Option<&String> {
self.figure_y_label.as_ref()
}
pub fn overlay_y_label_for_axes(&self, axes_index: usize) -> Option<&String> {
self.last_figure
.as_ref()
.and_then(|f| f.axes_metadata(axes_index))
.and_then(|m| m.y_label.as_ref())
}
pub fn overlay_y_label_style_for_axes(&self, axes_index: usize) -> Option<&TextStyle> {
self.last_figure
.as_ref()
.and_then(|f| f.axes_metadata(axes_index))
.map(|m| &m.y_label_style)
}
pub fn overlay_z_label(&self) -> Option<&String> {
self.figure_z_label.as_ref()
}
pub fn overlay_z_label_for_axes(&self, axes_index: usize) -> Option<&String> {
self.last_figure
.as_ref()
.and_then(|f| f.axes_metadata(axes_index))
.and_then(|m| m.z_label.as_ref())
}
pub fn overlay_z_label_style_for_axes(&self, axes_index: usize) -> Option<&TextStyle> {
self.last_figure
.as_ref()
.and_then(|f| f.axes_metadata(axes_index))
.map(|m| &m.z_label_style)
}
pub fn active_axes_pie_labels(&self) -> Vec<(String, glam::Vec2)> {
let Some(fig) = self.last_figure.as_ref() else {
return Vec::new();
};
fig.pie_labels_for_axes(fig.active_axes_index)
.into_iter()
.map(|entry| (entry.label, entry.position))
.collect()
}
pub fn pie_labels_for_axes(&self, axes_index: usize) -> Vec<(String, glam::Vec2)> {
let Some(fig) = self.last_figure.as_ref() else {
return Vec::new();
};
fig.pie_labels_for_axes(axes_index)
.into_iter()
.map(|entry| (entry.label, entry.position))
.collect()
}
pub fn world_text_annotations_for_axes(
&self,
axes_index: usize,
) -> Vec<(glam::Vec3, String, TextStyle)> {
self.last_figure
.as_ref()
.map(|f| {
f.axes_text_annotations(axes_index)
.iter()
.map(|annotation| {
(
annotation.position,
annotation.text.clone(),
annotation.style.clone(),
)
})
.collect()
})
.unwrap_or_default()
}
pub fn world_axis_label_annotations_for_axes(
&self,
axes_index: usize,
) -> Vec<(glam::Vec3, String, TextStyle)> {
let Some(fig) = self.last_figure.as_ref() else {
return Vec::new();
};
let Some(meta) = fig.axes_metadata(axes_index) else {
return Vec::new();
};
let Some(bounds) = self.axes_bounds(axes_index) else {
return Vec::new();
};
let dx = (bounds.max.x - bounds.min.x).abs().max(1.0e-3);
let dy = (bounds.max.y - bounds.min.y).abs().max(1.0e-3);
let dz = (bounds.max.z - bounds.min.z).abs().max(1.0e-3);
let camera = self
.axes_camera(axes_index)
.or_else(|| Some(self.camera()))
.expect("plot renderer must always have a camera");
let center = (bounds.min + bounds.max) * 0.5;
let cam_delta = camera.position - center;
let sx = if cam_delta.x >= 0.0 { 1.0 } else { -1.0 };
let sy = if cam_delta.y >= 0.0 { 1.0 } else { -1.0 };
let sz = if cam_delta.z >= 0.0 { 1.0 } else { -1.0 };
let x_anchor = glam::Vec3::new(bounds.min.x + dx * 0.82, bounds.min.y, bounds.min.z)
+ glam::Vec3::new(0.0, -sy * dy * 0.10, -sz * dz * 0.08);
let y_anchor = glam::Vec3::new(bounds.min.x, bounds.min.y + dy * 0.82, bounds.min.z)
+ glam::Vec3::new(-sx * dx * 0.10, 0.0, -sz * dz * 0.08);
let z_anchor = glam::Vec3::new(bounds.min.x, bounds.min.y, bounds.min.z + dz * 0.82)
+ glam::Vec3::new(-sx * dx * 0.08, -sy * dy * 0.08, 0.0);
let mut out = Vec::new();
if let Some(label) = meta.x_label.clone().filter(|s| !s.is_empty()) {
out.push((x_anchor, label, meta.x_label_style.clone()));
}
if let Some(label) = meta.y_label.clone().filter(|s| !s.is_empty()) {
out.push((y_anchor, label, meta.y_label_style.clone()));
}
if let Some(label) = meta.z_label.clone().filter(|s| !s.is_empty()) {
out.push((z_anchor, label, meta.z_label_style.clone()));
}
out
}
pub fn overlay_show_legend(&self) -> bool {
self.figure_show_legend
}
pub fn overlay_show_legend_for_axes(&self, axes_index: usize) -> bool {
self.last_figure
.as_ref()
.and_then(|f| f.axes_metadata(axes_index))
.map(|m| m.legend_enabled)
.unwrap_or(self.figure_show_legend)
}
pub fn overlay_legend_entries(&self) -> &Vec<LegendEntry> {
&self.legend_entries
}
pub fn overlay_legend_entries_for_axes(&self, axes_index: usize) -> Vec<LegendEntry> {
self.last_figure
.as_ref()
.map(|f| f.legend_entries_for_axes(axes_index))
.unwrap_or_default()
}
pub fn overlay_x_log(&self) -> bool {
self.figure_x_log
}
pub fn overlay_x_log_for_axes(&self, axes_index: usize) -> bool {
self.last_figure
.as_ref()
.and_then(|f| f.axes_metadata(axes_index))
.map(|m| m.x_log)
.unwrap_or(self.figure_x_log)
}
pub fn overlay_y_log(&self) -> bool {
self.figure_y_log
}
pub fn overlay_y_log_for_axes(&self, axes_index: usize) -> bool {
self.last_figure
.as_ref()
.and_then(|f| f.axes_metadata(axes_index))
.map(|m| m.y_log)
.unwrap_or(self.figure_y_log)
}
pub fn overlay_colormap(&self) -> ColorMap {
self.figure_colormap
}
pub fn overlay_colorbar_enabled(&self) -> bool {
self.figure_colorbar_enabled
}
pub fn figure_axes_grid(&self) -> (usize, usize) {
self.last_figure
.as_ref()
.map(|f| f.axes_grid())
.unwrap_or((1, 1))
}
pub fn overlay_categorical_labels(&self) -> Option<(bool, &Vec<String>)> {
if let (Some(is_x), Some(labels)) = (
&self.figure_categorical_is_x,
&self.figure_categorical_labels,
) {
Some((*is_x, labels))
} else {
None
}
}
pub fn overlay_categorical_labels_for_axes(
&self,
axes_index: usize,
) -> Option<(bool, Vec<String>)> {
self.last_figure
.as_ref()
.and_then(|f| f.categorical_axis_labels_for_axes(axes_index))
}
pub fn overlay_histogram_edges_for_axes(&self, axes_index: usize) -> Option<(bool, Vec<f64>)> {
self.last_figure
.as_ref()
.and_then(|f| f.histogram_axis_edges_for_axes(axes_index))
}
pub fn overlay_display_bounds_for_axes(
&self,
axes_index: usize,
) -> Option<(f64, f64, f64, f64)> {
self.display_bounds_for_axes(axes_index)
}
pub fn data_bounds(&self) -> Option<(f64, f64, f64, f64)> {
let base = self.data_bounds;
base.map(|(bx_min, bx_max, by_min, by_max)| {
let (mut x_min, mut x_max) = (bx_min, bx_max);
let (mut y_min, mut y_max) = (by_min, by_max);
if let Some((xl, xr)) = self.figure_x_limits {
x_min = xl;
x_max = xr;
}
if let Some((yl, yr)) = self.figure_y_limits {
y_min = yl;
y_max = yr;
}
(x_min, x_max, y_min, y_max)
})
}
pub fn axes_camera_mut(&mut self, idx: usize) -> Option<&mut Camera> {
self.axes_cameras.get_mut(idx)
}
pub fn view_bounds_for_axes(&self, idx: usize) -> Option<(f64, f64, f64, f64)> {
if let Some(cam) = self.axes_cameras.get(idx) {
if let crate::core::camera::ProjectionType::Orthographic {
left,
right,
bottom,
top,
..
} = cam.projection
{
return Some((left as f64, right as f64, bottom as f64, top as f64));
}
}
None
}
pub fn axes_bounds(&self, axes_index: usize) -> Option<crate::core::BoundingBox> {
let mut min = Vec3::new(f32::INFINITY, f32::INFINITY, f32::INFINITY);
let mut max = Vec3::new(f32::NEG_INFINITY, f32::NEG_INFINITY, f32::NEG_INFINITY);
let mut saw_any = false;
for node in self.scene.get_visible_nodes() {
if node.axes_index != axes_index {
continue;
}
let Some(render_data) = &node.render_data else {
continue;
};
if let Some(bounds) = render_data.bounds {
min = min.min(bounds.min);
max = max.max(bounds.max);
saw_any = true;
continue;
}
for v in &render_data.vertices {
let p = Vec3::new(v.position[0], v.position[1], v.position[2]);
min = min.min(p);
max = max.max(p);
saw_any = true;
}
}
if !saw_any {
return None;
}
Some(crate::core::BoundingBox { min, max })
}
pub fn export_figure_clone(&self) -> crate::plots::Figure {
if let Some(f) = &self.last_figure {
return f.clone();
}
let mut fig = crate::plots::Figure::new();
fig.title = self.figure_title.clone();
fig.x_label = self.figure_x_label.clone();
fig.y_label = self.figure_y_label.clone();
fig.legend_enabled = self.figure_show_legend;
fig.grid_enabled = self.figure_show_grid;
fig.box_enabled = self.figure_show_box;
fig.x_limits = self.figure_x_limits;
fig.y_limits = self.figure_y_limits;
fig.x_log = self.figure_x_log;
fig.y_log = self.figure_y_log;
fig.axis_equal = self.figure_axis_equal;
fig.colormap = self.figure_colormap;
fig.colorbar_enabled = self.figure_colorbar_enabled;
let (rows, cols) = self.figure_axes_grid();
fig.set_subplot_grid(rows, cols);
fig
}
}
pub mod plot_utils {
pub fn generate_major_ticks(min: f64, max: f64) -> Vec<f64> {
if !(min.is_finite() && max.is_finite()) || max <= min {
return Vec::new();
}
let range = (max - min).max(1e-9);
let step = calculate_tick_interval(range);
if !(step.is_finite() && step > 0.0) {
return Vec::new();
}
let mut ticks = Vec::new();
let mut value = (min / step).ceil() * step;
let epsilon = range * 1e-6 + step * 1e-6;
while value <= max + epsilon {
let snapped = if value.abs() < epsilon { 0.0 } else { value };
ticks.push(snapped);
value += step;
if ticks.len() > 64 {
break;
}
}
let endpoint_tol = step * 0.18;
let near_min = ticks.iter().any(|t| (*t - min).abs() <= endpoint_tol);
let near_max = ticks.iter().any(|t| (*t - max).abs() <= endpoint_tol);
if !near_min {
ticks.insert(0, min);
}
if !near_max {
ticks.push(max);
}
ticks.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
ticks.dedup_by(|a, b| (*a - *b).abs() <= endpoint_tol * 0.5);
ticks
}
pub fn calculate_tick_interval(range: f64) -> f64 {
let magnitude = 10.0_f64.powf(range.log10().floor());
let normalized = range / magnitude;
let nice_interval = if normalized <= 1.0 {
0.2
} else if normalized <= 2.0 {
0.5
} else if normalized <= 5.0 {
1.0
} else {
2.0
};
nice_interval * magnitude
}
pub fn format_tick_label(value: f64) -> String {
fn trim_fixed(mut s: String) -> String {
if s.contains('.') {
while s.ends_with('0') {
s.pop();
}
if s.ends_with('.') {
s.pop();
}
}
if s == "-0" {
"0".to_string()
} else {
s
}
}
if value.abs() < 0.001 {
"0".to_string()
} else if value.abs() >= 1000.0 || value.fract().abs() < 0.0005 {
format!("{value:.0}")
} else if value.abs() < 0.1 {
trim_fixed(format!("{value:.3}"))
} else if value.abs() < 10.0 {
trim_fixed(format!("{value:.2}"))
} else {
trim_fixed(format!("{value:.1}"))
}
}
pub fn generate_grid_lines(
bounds: (f64, f64, f64, f64),
plot_rect: (f32, f32, f32, f32), ) -> Vec<(f32, f32, f32, f32)> {
let (x_min, x_max, y_min, y_max) = bounds;
let (left, right, bottom, top) = plot_rect;
let mut lines = Vec::new();
let x_range = x_max - x_min;
let x_interval = calculate_tick_interval(x_range);
let mut x_val = (x_min / x_interval).ceil() * x_interval;
while x_val <= x_max {
let x_screen = left + ((x_val - x_min) / x_range) as f32 * (right - left);
lines.push((x_screen, bottom, x_screen, top));
x_val += x_interval;
}
let y_range = y_max - y_min;
let y_interval = calculate_tick_interval(y_range);
let mut y_val = (y_min / y_interval).ceil() * y_interval;
while y_val <= y_max {
let y_screen = bottom + ((y_val - y_min) / y_range) as f32 * (top - bottom);
lines.push((left, y_screen, right, y_screen));
y_val += y_interval;
}
lines
}
}