Skip to main content

runmat_plot/core/
scene.rs

1//! Scene graph system for organizing and managing plot objects
2//!
3//! Provides hierarchical organization of plot elements with efficient
4//! culling, level-of-detail, and batch rendering capabilities.
5
6use crate::core::renderer::{PipelineType, Vertex};
7use bytemuck::{Pod, Zeroable};
8use glam::{Mat4, Vec3, Vec4};
9use std::collections::HashMap;
10use std::sync::Arc;
11
12/// Unique identifier for scene nodes
13pub type NodeId = u64;
14
15/// Scene node representing a renderable object
16#[derive(Debug, Clone)]
17pub struct SceneNode {
18    pub id: NodeId,
19    pub name: String,
20    pub transform: Mat4,
21    pub visible: bool,
22    pub cast_shadows: bool,
23    pub receive_shadows: bool,
24
25    /// Axes index this node belongs to (for subplots). Row-major index in [0, rows*cols).
26    pub axes_index: usize,
27
28    // Hierarchy
29    pub parent: Option<NodeId>,
30    pub children: Vec<NodeId>,
31
32    // Rendering data
33    pub render_data: Option<RenderData>,
34
35    // Bounding box for culling
36    pub bounds: BoundingBox,
37
38    // Level of detail settings
39    pub lod_levels: Vec<LodLevel>,
40    pub current_lod: usize,
41}
42
43/// Rendering data for a scene node
44#[derive(Debug, Clone)]
45pub struct RenderData {
46    pub pipeline_type: PipelineType,
47    pub vertices: Vec<Vertex>,
48    pub indices: Option<Vec<u32>>,
49    pub gpu_vertices: Option<GpuVertexBuffer>,
50    /// Data-space bounds for this render item (used for camera fitting / direct mapping).
51    ///
52    /// GPU-backed plots often have `vertices=[]`, so bounds must be carried explicitly.
53    pub bounds: Option<BoundingBox>,
54    pub material: Material,
55    pub draw_calls: Vec<DrawCall>,
56    /// Optional image payload for textured rendering
57    pub image: Option<ImageData>,
58}
59
60impl RenderData {
61    pub fn vertex_count(&self) -> usize {
62        if !self.vertices.is_empty() {
63            self.vertices.len()
64        } else if let Some(buffer) = &self.gpu_vertices {
65            buffer.vertex_count
66        } else {
67            0
68        }
69    }
70}
71
72/// Optional GPU-resident vertex storage supplied by higher-level systems.
73#[derive(Debug, Clone)]
74pub struct GpuVertexBuffer {
75    pub buffer: Arc<wgpu::Buffer>,
76    pub vertex_count: usize,
77    /// Optional indirect draw arguments buffer (GPU-driven vertex count).
78    ///
79    /// When present, renderers should prefer `draw_indirect` over issuing explicit draw ranges
80    /// based on `vertex_count`. This avoids CPU readbacks on wasm/WebGPU.
81    pub indirect: Option<GpuIndirectDraw>,
82}
83
84impl GpuVertexBuffer {
85    pub fn new(buffer: Arc<wgpu::Buffer>, vertex_count: usize) -> Self {
86        Self {
87            buffer,
88            vertex_count,
89            indirect: None,
90        }
91    }
92
93    pub fn with_indirect(
94        buffer: Arc<wgpu::Buffer>,
95        vertex_count: usize,
96        indirect_args: Arc<wgpu::Buffer>,
97    ) -> Self {
98        Self {
99            buffer,
100            vertex_count,
101            indirect: Some(GpuIndirectDraw {
102                args: indirect_args,
103                offset: 0,
104            }),
105        }
106    }
107}
108
109/// GPU-driven draw call arguments for `draw_indirect`.
110#[derive(Debug, Clone)]
111pub struct GpuIndirectDraw {
112    pub args: Arc<wgpu::Buffer>,
113    pub offset: u64,
114}
115
116/// POD layout matching `wgpu`'s non-indexed indirect draw arguments.
117///
118/// This buffer is written once from the CPU to set instance_count=1, and then
119/// shaders atomically update `vertex_count` to drive draws without CPU readback.
120#[repr(C)]
121#[derive(Clone, Copy, Debug, Pod, Zeroable)]
122pub struct DrawIndirectArgsRaw {
123    pub vertex_count: u32,
124    pub instance_count: u32,
125    pub first_vertex: u32,
126    pub first_instance: u32,
127}
128
129/// CPU-side image payload for textured rendering
130#[derive(Debug, Clone)]
131pub enum ImageData {
132    /// 8-bit RGBA image (row-major, top-to-bottom rows)
133    Rgba8 {
134        width: u32,
135        height: u32,
136        data: Vec<u8>,
137    },
138}
139
140/// Material properties for rendering
141#[derive(Debug, Clone)]
142pub struct Material {
143    pub albedo: Vec4,
144    pub roughness: f32,
145    pub metallic: f32,
146    pub emissive: Vec4,
147    pub alpha_mode: AlphaMode,
148    pub double_sided: bool,
149}
150
151impl Default for Material {
152    fn default() -> Self {
153        Self {
154            albedo: Vec4::new(1.0, 1.0, 1.0, 1.0),
155            roughness: 0.5,
156            metallic: 0.0,
157            emissive: Vec4::ZERO,
158            alpha_mode: AlphaMode::Opaque,
159            double_sided: false,
160        }
161    }
162}
163
164/// Alpha blending mode
165#[derive(Debug, Clone, Copy, PartialEq, Eq)]
166pub enum AlphaMode {
167    Opaque,
168    Mask { cutoff: u8 },
169    Blend,
170}
171
172/// Level of detail configuration
173#[derive(Debug, Clone)]
174pub struct LodLevel {
175    pub distance: f32,
176    pub vertex_count: usize,
177    pub index_count: Option<usize>,
178    pub simplification_ratio: f32,
179}
180
181/// Draw call for efficient batching
182#[derive(Debug, Clone)]
183pub struct DrawCall {
184    pub vertex_offset: usize,
185    pub vertex_count: usize,
186    pub index_offset: Option<usize>,
187    pub index_count: Option<usize>,
188    pub instance_count: usize,
189}
190
191/// Axis-aligned bounding box
192#[derive(Debug, Clone, Copy)]
193pub struct BoundingBox {
194    pub min: Vec3,
195    pub max: Vec3,
196}
197
198impl Default for BoundingBox {
199    fn default() -> Self {
200        Self {
201            min: Vec3::splat(f32::INFINITY),
202            max: Vec3::splat(f32::NEG_INFINITY),
203        }
204    }
205}
206
207impl BoundingBox {
208    pub fn new(min: Vec3, max: Vec3) -> Self {
209        Self { min, max }
210    }
211
212    pub fn from_points(points: &[Vec3]) -> Self {
213        if points.is_empty() {
214            return Self::default();
215        }
216
217        let mut min = points[0];
218        let mut max = points[0];
219
220        for &point in points.iter().skip(1) {
221            min = min.min(point);
222            max = max.max(point);
223        }
224
225        Self { min, max }
226    }
227
228    pub fn center(&self) -> Vec3 {
229        (self.min + self.max) / 2.0
230    }
231
232    pub fn size(&self) -> Vec3 {
233        self.max - self.min
234    }
235
236    pub fn expand(&mut self, point: Vec3) {
237        self.min = self.min.min(point);
238        self.max = self.max.max(point);
239    }
240
241    pub fn expand_by_box(&mut self, other: &BoundingBox) {
242        self.min = self.min.min(other.min);
243        self.max = self.max.max(other.max);
244    }
245
246    pub fn transform(&self, transform: &Mat4) -> Self {
247        let corners = [
248            Vec3::new(self.min.x, self.min.y, self.min.z),
249            Vec3::new(self.max.x, self.min.y, self.min.z),
250            Vec3::new(self.min.x, self.max.y, self.min.z),
251            Vec3::new(self.max.x, self.max.y, self.min.z),
252            Vec3::new(self.min.x, self.min.y, self.max.z),
253            Vec3::new(self.max.x, self.min.y, self.max.z),
254            Vec3::new(self.min.x, self.max.y, self.max.z),
255            Vec3::new(self.max.x, self.max.y, self.max.z),
256        ];
257
258        let transformed_corners: Vec<Vec3> = corners
259            .iter()
260            .map(|&corner| (*transform * corner.extend(1.0)).truncate())
261            .collect();
262
263        Self::from_points(&transformed_corners)
264    }
265
266    pub fn intersects(&self, other: &BoundingBox) -> bool {
267        self.min.x <= other.max.x
268            && self.max.x >= other.min.x
269            && self.min.y <= other.max.y
270            && self.max.y >= other.min.y
271            && self.min.z <= other.max.z
272            && self.max.z >= other.min.z
273    }
274
275    pub fn contains_point(&self, point: Vec3) -> bool {
276        point.x >= self.min.x
277            && point.x <= self.max.x
278            && point.y >= self.min.y
279            && point.y <= self.max.y
280            && point.z >= self.min.z
281            && point.z <= self.max.z
282    }
283
284    /// Create a union of two bounding boxes
285    pub fn union(&self, other: &BoundingBox) -> BoundingBox {
286        BoundingBox {
287            min: Vec3::new(
288                self.min.x.min(other.min.x),
289                self.min.y.min(other.min.y),
290                self.min.z.min(other.min.z),
291            ),
292            max: Vec3::new(
293                self.max.x.max(other.max.x),
294                self.max.y.max(other.max.y),
295                self.max.z.max(other.max.z),
296            ),
297        }
298    }
299}
300
301/// Scene graph managing hierarchical plot objects
302pub struct Scene {
303    nodes: HashMap<NodeId, SceneNode>,
304    root_nodes: Vec<NodeId>,
305    next_id: NodeId,
306
307    // Cached data for optimization
308    world_bounds: BoundingBox,
309    bounds_dirty: bool,
310
311    // Culling and LOD
312    frustum: Option<Frustum>,
313    camera_position: Vec3,
314}
315
316impl Default for Scene {
317    fn default() -> Self {
318        Self::new()
319    }
320}
321
322impl Scene {
323    pub fn new() -> Self {
324        Self {
325            nodes: HashMap::new(),
326            root_nodes: Vec::new(),
327            next_id: 1,
328            world_bounds: BoundingBox::default(),
329            bounds_dirty: true,
330            frustum: None,
331            camera_position: Vec3::ZERO,
332        }
333    }
334
335    /// Add a new node to the scene
336    pub fn add_node(&mut self, mut node: SceneNode) -> NodeId {
337        let id = self.next_id;
338        self.next_id += 1;
339
340        node.id = id;
341
342        // Add to parent's children if specified
343        if let Some(parent_id) = node.parent {
344            if let Some(parent) = self.nodes.get_mut(&parent_id) {
345                parent.children.push(id);
346            }
347        } else {
348            self.root_nodes.push(id);
349        }
350
351        self.nodes.insert(id, node);
352        self.bounds_dirty = true;
353        id
354    }
355
356    /// Remove a node and all its children
357    pub fn remove_node(&mut self, id: NodeId) -> bool {
358        // Get the node data first to avoid borrowing conflicts
359        let (parent_id, children) = if let Some(node) = self.nodes.get(&id) {
360            (node.parent, node.children.clone())
361        } else {
362            return false;
363        };
364
365        // Remove from parent's children
366        if let Some(parent_id) = parent_id {
367            if let Some(parent) = self.nodes.get_mut(&parent_id) {
368                parent.children.retain(|&child_id| child_id != id);
369            }
370        } else {
371            self.root_nodes.retain(|&root_id| root_id != id);
372        }
373
374        // Recursively remove children
375        for child_id in children {
376            self.remove_node(child_id);
377        }
378
379        self.nodes.remove(&id);
380        self.bounds_dirty = true;
381        true
382    }
383
384    /// Get a node by ID
385    pub fn get_node(&self, id: NodeId) -> Option<&SceneNode> {
386        self.nodes.get(&id)
387    }
388
389    /// Get a mutable node by ID
390    pub fn get_node_mut(&mut self, id: NodeId) -> Option<&mut SceneNode> {
391        if self.nodes.contains_key(&id) {
392            self.bounds_dirty = true;
393        }
394        self.nodes.get_mut(&id)
395    }
396
397    /// Update world transform for a node and its children
398    pub fn update_transforms(&mut self, root_transform: Mat4) {
399        for &root_id in &self.root_nodes.clone() {
400            self.update_node_transform(root_id, root_transform);
401        }
402    }
403
404    fn update_node_transform(&mut self, node_id: NodeId, parent_transform: Mat4) {
405        if let Some(node) = self.nodes.get_mut(&node_id) {
406            let world_transform = parent_transform * node.transform;
407
408            // Update bounding box
409            if let Some(render_data) = &node.render_data {
410                let local_bounds = BoundingBox::from_points(
411                    &render_data
412                        .vertices
413                        .iter()
414                        .map(|v| Vec3::from_array(v.position))
415                        .collect::<Vec<_>>(),
416                );
417                node.bounds = local_bounds.transform(&world_transform);
418            }
419
420            // Recursively update children
421            let children = node.children.clone();
422            for child_id in children {
423                self.update_node_transform(child_id, world_transform);
424            }
425        }
426    }
427
428    /// Get the overall bounding box of the scene
429    pub fn world_bounds(&mut self) -> BoundingBox {
430        if self.bounds_dirty {
431            self.update_world_bounds();
432        }
433        self.world_bounds
434    }
435
436    fn update_world_bounds(&mut self) {
437        self.world_bounds = BoundingBox::default();
438
439        for node in self.nodes.values() {
440            if node.visible {
441                self.world_bounds.expand_by_box(&node.bounds);
442            }
443        }
444
445        self.bounds_dirty = false;
446    }
447
448    /// Set camera position for LOD calculations
449    pub fn set_camera_position(&mut self, position: Vec3) {
450        self.camera_position = position;
451        self.update_lod();
452    }
453
454    /// Update level of detail for all nodes based on camera distance
455    fn update_lod(&mut self) {
456        for node in self.nodes.values_mut() {
457            if !node.lod_levels.is_empty() {
458                let distance = node.bounds.center().distance(self.camera_position);
459
460                // Find appropriate LOD level
461                let mut lod_index = node.lod_levels.len() - 1;
462                for (i, lod) in node.lod_levels.iter().enumerate() {
463                    if distance <= lod.distance {
464                        lod_index = i;
465                        break;
466                    }
467                }
468
469                node.current_lod = lod_index;
470            }
471        }
472    }
473
474    /// Get visible nodes for rendering (with frustum culling)
475    pub fn get_visible_nodes(&self) -> Vec<&SceneNode> {
476        self.nodes
477            .values()
478            .filter(|node| {
479                node.visible && node.render_data.is_some() && self.is_node_in_frustum(node)
480            })
481            .collect()
482    }
483
484    fn is_node_in_frustum(&self, node: &SceneNode) -> bool {
485        // If no frustum is set, all nodes are visible
486        if let Some(ref frustum) = self.frustum {
487            frustum.intersects_box(&node.bounds)
488        } else {
489            true
490        }
491    }
492
493    /// Set frustum for culling
494    pub fn set_frustum(&mut self, frustum: Frustum) {
495        self.frustum = Some(frustum);
496    }
497
498    /// Clear all nodes
499    pub fn clear(&mut self) {
500        self.nodes.clear();
501        self.root_nodes.clear();
502        self.bounds_dirty = true;
503    }
504
505    /// Get statistics about the scene
506    pub fn statistics(&self) -> SceneStatistics {
507        let visible_nodes = self.nodes.values().filter(|n| n.visible).count();
508        let total_vertices: usize = self
509            .nodes
510            .values()
511            .filter_map(|n| n.render_data.as_ref())
512            .map(|rd| rd.vertices.len())
513            .sum();
514        let total_triangles: usize = self
515            .nodes
516            .values()
517            .filter_map(|n| n.render_data.as_ref())
518            .filter(|rd| rd.pipeline_type == PipelineType::Triangles)
519            .map(|rd| {
520                rd.indices
521                    .as_ref()
522                    .map_or(rd.vertices.len() / 3, |i| i.len() / 3)
523            })
524            .sum();
525
526        SceneStatistics {
527            total_nodes: self.nodes.len(),
528            visible_nodes,
529            total_vertices,
530            total_triangles,
531        }
532    }
533}
534
535/// View frustum for culling
536#[derive(Debug, Clone)]
537pub struct Frustum {
538    pub planes: [Plane; 6], // left, right, bottom, top, near, far
539}
540
541impl Frustum {
542    pub fn from_view_proj(view_proj: Mat4) -> Self {
543        let m = view_proj.to_cols_array_2d();
544
545        // Extract frustum planes from view-projection matrix
546        let planes = [
547            // Left plane
548            Plane::new(
549                m[0][3] + m[0][0],
550                m[1][3] + m[1][0],
551                m[2][3] + m[2][0],
552                m[3][3] + m[3][0],
553            ),
554            // Right plane
555            Plane::new(
556                m[0][3] - m[0][0],
557                m[1][3] - m[1][0],
558                m[2][3] - m[2][0],
559                m[3][3] - m[3][0],
560            ),
561            // Bottom plane
562            Plane::new(
563                m[0][3] + m[0][1],
564                m[1][3] + m[1][1],
565                m[2][3] + m[2][1],
566                m[3][3] + m[3][1],
567            ),
568            // Top plane
569            Plane::new(
570                m[0][3] - m[0][1],
571                m[1][3] - m[1][1],
572                m[2][3] - m[2][1],
573                m[3][3] - m[3][1],
574            ),
575            // Near plane
576            Plane::new(
577                m[0][3] + m[0][2],
578                m[1][3] + m[1][2],
579                m[2][3] + m[2][2],
580                m[3][3] + m[3][2],
581            ),
582            // Far plane
583            Plane::new(
584                m[0][3] - m[0][2],
585                m[1][3] - m[1][2],
586                m[2][3] - m[2][2],
587                m[3][3] - m[3][2],
588            ),
589        ];
590
591        Self { planes }
592    }
593
594    pub fn intersects_box(&self, bbox: &BoundingBox) -> bool {
595        for plane in &self.planes {
596            if plane.distance_to_box(bbox) > 0.0 {
597                return false; // Box is outside this plane
598            }
599        }
600        true // Box intersects or is inside frustum
601    }
602}
603
604/// Plane equation ax + by + cz + d = 0
605#[derive(Debug, Clone, Copy)]
606pub struct Plane {
607    pub normal: Vec3,
608    pub distance: f32,
609}
610
611impl Plane {
612    pub fn new(a: f32, b: f32, c: f32, d: f32) -> Self {
613        let normal = Vec3::new(a, b, c);
614        let length = normal.length();
615
616        Self {
617            normal: normal / length,
618            distance: d / length,
619        }
620    }
621
622    pub fn distance_to_point(&self, point: Vec3) -> f32 {
623        self.normal.dot(point) + self.distance
624    }
625
626    pub fn distance_to_box(&self, bbox: &BoundingBox) -> f32 {
627        // Find the positive vertex (farthest in direction of normal)
628        let positive_vertex = Vec3::new(
629            if self.normal.x >= 0.0 {
630                bbox.max.x
631            } else {
632                bbox.min.x
633            },
634            if self.normal.y >= 0.0 {
635                bbox.max.y
636            } else {
637                bbox.min.y
638            },
639            if self.normal.z >= 0.0 {
640                bbox.max.z
641            } else {
642                bbox.min.z
643            },
644        );
645
646        self.distance_to_point(positive_vertex)
647    }
648}
649
650/// Scene statistics for debugging and optimization
651#[derive(Debug, Clone)]
652pub struct SceneStatistics {
653    pub total_nodes: usize,
654    pub visible_nodes: usize,
655    pub total_vertices: usize,
656    pub total_triangles: usize,
657}
658
659#[cfg(test)]
660mod tests {
661    use super::*;
662
663    #[test]
664    fn test_bounding_box_creation() {
665        let points = vec![
666            Vec3::new(-1.0, -1.0, -1.0),
667            Vec3::new(1.0, 1.0, 1.0),
668            Vec3::new(0.0, 0.0, 0.0),
669        ];
670
671        let bbox = BoundingBox::from_points(&points);
672        assert_eq!(bbox.min, Vec3::new(-1.0, -1.0, -1.0));
673        assert_eq!(bbox.max, Vec3::new(1.0, 1.0, 1.0));
674        assert_eq!(bbox.center(), Vec3::ZERO);
675    }
676
677    #[test]
678    fn test_scene_node_hierarchy() {
679        let mut scene = Scene::new();
680
681        let parent_node = SceneNode {
682            id: 0,
683            name: "Parent".to_string(),
684            transform: Mat4::IDENTITY,
685            visible: true,
686            cast_shadows: true,
687            receive_shadows: true,
688            axes_index: 0,
689            parent: None,
690            children: Vec::new(),
691            render_data: None,
692            bounds: BoundingBox::default(),
693            lod_levels: Vec::new(),
694            current_lod: 0,
695        };
696
697        let parent_id = scene.add_node(parent_node);
698
699        let child_node = SceneNode {
700            id: 0,
701            name: "Child".to_string(),
702            transform: Mat4::from_translation(Vec3::new(1.0, 0.0, 0.0)),
703            visible: true,
704            cast_shadows: true,
705            receive_shadows: true,
706            axes_index: 0,
707            parent: Some(parent_id),
708            children: Vec::new(),
709            render_data: None,
710            bounds: BoundingBox::default(),
711            lod_levels: Vec::new(),
712            current_lod: 0,
713        };
714
715        let child_id = scene.add_node(child_node);
716
717        // Verify hierarchy
718        let parent = scene.get_node(parent_id).unwrap();
719        assert!(parent.children.contains(&child_id));
720
721        let child = scene.get_node(child_id).unwrap();
722        assert_eq!(child.parent, Some(parent_id));
723    }
724}