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