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, node: SceneNode) -> NodeId {
337        while self.nodes.contains_key(&self.next_id) {
338            self.next_id = self.next_id.checked_add(1).unwrap_or(1);
339        }
340        let id = self.next_id;
341        self.next_id = self.next_id.checked_add(1).unwrap_or(1);
342
343        self.insert_node_with_id(id, node)
344    }
345
346    /// Add a node using its existing ID when that ID is available.
347    ///
348    /// Geometry scenes use stable IDs so presentation refreshes can update the
349    /// same scene nodes without re-identifying them through insertion order.
350    pub fn add_node_preserving_id(&mut self, node: SceneNode) -> NodeId {
351        let id = node.id;
352        if id != 0 && !self.nodes.contains_key(&id) {
353            return self.insert_node_with_id(id, node);
354        }
355        self.add_node(node)
356    }
357
358    fn insert_node_with_id(&mut self, id: NodeId, mut node: SceneNode) -> NodeId {
359        node.id = id;
360
361        // Add to parent's children if specified
362        if let Some(parent_id) = node.parent {
363            if let Some(parent) = self.nodes.get_mut(&parent_id) {
364                parent.children.push(id);
365            }
366        } else {
367            self.root_nodes.push(id);
368        }
369
370        self.nodes.insert(id, node);
371        self.bounds_dirty = true;
372        id
373    }
374
375    /// Remove a node and all its children
376    pub fn remove_node(&mut self, id: NodeId) -> bool {
377        // Get the node data first to avoid borrowing conflicts
378        let (parent_id, children) = if let Some(node) = self.nodes.get(&id) {
379            (node.parent, node.children.clone())
380        } else {
381            return false;
382        };
383
384        // Remove from parent's children
385        if let Some(parent_id) = parent_id {
386            if let Some(parent) = self.nodes.get_mut(&parent_id) {
387                parent.children.retain(|&child_id| child_id != id);
388            }
389        } else {
390            self.root_nodes.retain(|&root_id| root_id != id);
391        }
392
393        // Recursively remove children
394        for child_id in children {
395            self.remove_node(child_id);
396        }
397
398        self.nodes.remove(&id);
399        self.bounds_dirty = true;
400        true
401    }
402
403    /// Get a node by ID
404    pub fn get_node(&self, id: NodeId) -> Option<&SceneNode> {
405        self.nodes.get(&id)
406    }
407
408    /// Get a mutable node by ID
409    pub fn get_node_mut(&mut self, id: NodeId) -> Option<&mut SceneNode> {
410        if self.nodes.contains_key(&id) {
411            self.bounds_dirty = true;
412        }
413        self.nodes.get_mut(&id)
414    }
415
416    /// Visit all nodes mutably. Rendering code uses this for view-state changes
417    /// such as CAD display modes that should not rebuild the scene graph.
418    pub fn for_each_node_mut(&mut self, mut visit: impl FnMut(&mut SceneNode)) {
419        for node in self.nodes.values_mut() {
420            visit(node);
421        }
422        self.bounds_dirty = true;
423    }
424
425    /// Update world transform for a node and its children
426    pub fn update_transforms(&mut self, root_transform: Mat4) {
427        for &root_id in &self.root_nodes.clone() {
428            self.update_node_transform(root_id, root_transform);
429        }
430    }
431
432    fn update_node_transform(&mut self, node_id: NodeId, parent_transform: Mat4) {
433        if let Some(node) = self.nodes.get_mut(&node_id) {
434            let world_transform = parent_transform * node.transform;
435
436            // Update bounding box
437            if let Some(render_data) = &node.render_data {
438                let local_bounds = BoundingBox::from_points(
439                    &render_data
440                        .vertices
441                        .iter()
442                        .map(|v| Vec3::from_array(v.position))
443                        .collect::<Vec<_>>(),
444                );
445                node.bounds = local_bounds.transform(&world_transform);
446            }
447
448            // Recursively update children
449            let children = node.children.clone();
450            for child_id in children {
451                self.update_node_transform(child_id, world_transform);
452            }
453        }
454    }
455
456    /// Get the overall bounding box of the scene
457    pub fn world_bounds(&mut self) -> BoundingBox {
458        if self.bounds_dirty {
459            self.update_world_bounds();
460        }
461        self.world_bounds
462    }
463
464    fn update_world_bounds(&mut self) {
465        self.world_bounds = BoundingBox::default();
466
467        for node in self.nodes.values() {
468            if node.visible {
469                self.world_bounds.expand_by_box(&node.bounds);
470            }
471        }
472
473        self.bounds_dirty = false;
474    }
475
476    /// Set camera position for LOD calculations
477    pub fn set_camera_position(&mut self, position: Vec3) {
478        self.camera_position = position;
479        self.update_lod();
480    }
481
482    /// Update level of detail for all nodes based on camera distance
483    fn update_lod(&mut self) {
484        for node in self.nodes.values_mut() {
485            if !node.lod_levels.is_empty() {
486                let distance = node.bounds.center().distance(self.camera_position);
487
488                // Find appropriate LOD level
489                let mut lod_index = node.lod_levels.len() - 1;
490                for (i, lod) in node.lod_levels.iter().enumerate() {
491                    if distance <= lod.distance {
492                        lod_index = i;
493                        break;
494                    }
495                }
496
497                node.current_lod = lod_index;
498            }
499        }
500    }
501
502    /// Get visible nodes for rendering (with frustum culling)
503    pub fn get_visible_nodes(&self) -> Vec<&SceneNode> {
504        self.nodes
505            .values()
506            .filter(|node| {
507                node.visible && node.render_data.is_some() && self.is_node_in_frustum(node)
508            })
509            .collect()
510    }
511
512    fn is_node_in_frustum(&self, node: &SceneNode) -> bool {
513        // If no frustum is set, all nodes are visible
514        if let Some(ref frustum) = self.frustum {
515            frustum.intersects_box(&node.bounds)
516        } else {
517            true
518        }
519    }
520
521    /// Set frustum for culling
522    pub fn set_frustum(&mut self, frustum: Frustum) {
523        self.frustum = Some(frustum);
524    }
525
526    /// Clear all nodes
527    pub fn clear(&mut self) {
528        self.nodes.clear();
529        self.root_nodes.clear();
530        self.bounds_dirty = true;
531    }
532
533    /// Get statistics about the scene
534    pub fn statistics(&self) -> SceneStatistics {
535        let visible_nodes = self.nodes.values().filter(|n| n.visible).count();
536        let total_vertices: usize = self
537            .nodes
538            .values()
539            .filter_map(|n| n.render_data.as_ref())
540            .map(|rd| rd.vertices.len())
541            .sum();
542        let total_triangles: usize = self
543            .nodes
544            .values()
545            .filter_map(|n| n.render_data.as_ref())
546            .filter(|rd| rd.pipeline_type == PipelineType::Triangles)
547            .map(|rd| {
548                rd.indices
549                    .as_ref()
550                    .map_or(rd.vertices.len() / 3, |i| i.len() / 3)
551            })
552            .sum();
553
554        SceneStatistics {
555            total_nodes: self.nodes.len(),
556            visible_nodes,
557            total_vertices,
558            total_triangles,
559        }
560    }
561}
562
563/// View frustum for culling
564#[derive(Debug, Clone)]
565pub struct Frustum {
566    pub planes: [Plane; 6], // left, right, bottom, top, near, far
567}
568
569impl Frustum {
570    pub fn from_view_proj(view_proj: Mat4) -> Self {
571        let m = view_proj.to_cols_array_2d();
572
573        // Extract frustum planes from view-projection matrix
574        let planes = [
575            // Left plane
576            Plane::new(
577                m[0][3] + m[0][0],
578                m[1][3] + m[1][0],
579                m[2][3] + m[2][0],
580                m[3][3] + m[3][0],
581            ),
582            // Right plane
583            Plane::new(
584                m[0][3] - m[0][0],
585                m[1][3] - m[1][0],
586                m[2][3] - m[2][0],
587                m[3][3] - m[3][0],
588            ),
589            // Bottom plane
590            Plane::new(
591                m[0][3] + m[0][1],
592                m[1][3] + m[1][1],
593                m[2][3] + m[2][1],
594                m[3][3] + m[3][1],
595            ),
596            // Top plane
597            Plane::new(
598                m[0][3] - m[0][1],
599                m[1][3] - m[1][1],
600                m[2][3] - m[2][1],
601                m[3][3] - m[3][1],
602            ),
603            // Near plane
604            Plane::new(
605                m[0][3] + m[0][2],
606                m[1][3] + m[1][2],
607                m[2][3] + m[2][2],
608                m[3][3] + m[3][2],
609            ),
610            // Far plane
611            Plane::new(
612                m[0][3] - m[0][2],
613                m[1][3] - m[1][2],
614                m[2][3] - m[2][2],
615                m[3][3] - m[3][2],
616            ),
617        ];
618
619        Self { planes }
620    }
621
622    pub fn intersects_box(&self, bbox: &BoundingBox) -> bool {
623        for plane in &self.planes {
624            if plane.distance_to_box(bbox) > 0.0 {
625                return false; // Box is outside this plane
626            }
627        }
628        true // Box intersects or is inside frustum
629    }
630}
631
632/// Plane equation ax + by + cz + d = 0
633#[derive(Debug, Clone, Copy)]
634pub struct Plane {
635    pub normal: Vec3,
636    pub distance: f32,
637}
638
639impl Plane {
640    pub fn new(a: f32, b: f32, c: f32, d: f32) -> Self {
641        let normal = Vec3::new(a, b, c);
642        let length = normal.length();
643
644        Self {
645            normal: normal / length,
646            distance: d / length,
647        }
648    }
649
650    pub fn distance_to_point(&self, point: Vec3) -> f32 {
651        self.normal.dot(point) + self.distance
652    }
653
654    pub fn distance_to_box(&self, bbox: &BoundingBox) -> f32 {
655        // Find the positive vertex (farthest in direction of normal)
656        let positive_vertex = Vec3::new(
657            if self.normal.x >= 0.0 {
658                bbox.max.x
659            } else {
660                bbox.min.x
661            },
662            if self.normal.y >= 0.0 {
663                bbox.max.y
664            } else {
665                bbox.min.y
666            },
667            if self.normal.z >= 0.0 {
668                bbox.max.z
669            } else {
670                bbox.min.z
671            },
672        );
673
674        self.distance_to_point(positive_vertex)
675    }
676}
677
678/// Scene statistics for debugging and optimization
679#[derive(Debug, Clone)]
680pub struct SceneStatistics {
681    pub total_nodes: usize,
682    pub visible_nodes: usize,
683    pub total_vertices: usize,
684    pub total_triangles: usize,
685}
686
687#[cfg(test)]
688mod tests {
689    use super::*;
690
691    #[test]
692    fn test_bounding_box_creation() {
693        let points = vec![
694            Vec3::new(-1.0, -1.0, -1.0),
695            Vec3::new(1.0, 1.0, 1.0),
696            Vec3::new(0.0, 0.0, 0.0),
697        ];
698
699        let bbox = BoundingBox::from_points(&points);
700        assert_eq!(bbox.min, Vec3::new(-1.0, -1.0, -1.0));
701        assert_eq!(bbox.max, Vec3::new(1.0, 1.0, 1.0));
702        assert_eq!(bbox.center(), Vec3::ZERO);
703    }
704
705    #[test]
706    fn test_scene_node_hierarchy() {
707        let mut scene = Scene::new();
708
709        let parent_node = SceneNode {
710            id: 0,
711            name: "Parent".to_string(),
712            transform: Mat4::IDENTITY,
713            visible: true,
714            cast_shadows: true,
715            receive_shadows: true,
716            axes_index: 0,
717            parent: None,
718            children: Vec::new(),
719            render_data: None,
720            bounds: BoundingBox::default(),
721            lod_levels: Vec::new(),
722            current_lod: 0,
723        };
724
725        let parent_id = scene.add_node(parent_node);
726
727        let child_node = SceneNode {
728            id: 0,
729            name: "Child".to_string(),
730            transform: Mat4::from_translation(Vec3::new(1.0, 0.0, 0.0)),
731            visible: true,
732            cast_shadows: true,
733            receive_shadows: true,
734            axes_index: 0,
735            parent: Some(parent_id),
736            children: Vec::new(),
737            render_data: None,
738            bounds: BoundingBox::default(),
739            lod_levels: Vec::new(),
740            current_lod: 0,
741        };
742
743        let child_id = scene.add_node(child_node);
744
745        // Verify hierarchy
746        let parent = scene.get_node(parent_id).unwrap();
747        assert!(parent.children.contains(&child_id));
748
749        let child = scene.get_node(child_id).unwrap();
750        assert_eq!(child.parent, Some(parent_id));
751    }
752
753    #[test]
754    fn scene_can_preserve_explicit_node_ids() {
755        let mut scene = Scene::new();
756        let explicit_id = 42;
757
758        let node = SceneNode {
759            id: explicit_id,
760            name: "Stable".to_string(),
761            transform: Mat4::IDENTITY,
762            visible: true,
763            cast_shadows: false,
764            receive_shadows: false,
765            axes_index: 0,
766            parent: None,
767            children: Vec::new(),
768            render_data: None,
769            bounds: BoundingBox::default(),
770            lod_levels: Vec::new(),
771            current_lod: 0,
772        };
773
774        let inserted_id = scene.add_node_preserving_id(node);
775
776        assert_eq!(inserted_id, explicit_id);
777        assert!(scene.get_node(explicit_id).is_some());
778
779        let generated_id = scene.add_node(SceneNode {
780            id: explicit_id,
781            name: "Generated".to_string(),
782            transform: Mat4::IDENTITY,
783            visible: true,
784            cast_shadows: false,
785            receive_shadows: false,
786            axes_index: 0,
787            parent: None,
788            children: Vec::new(),
789            render_data: None,
790            bounds: BoundingBox::default(),
791            lod_levels: Vec::new(),
792            current_lod: 0,
793        });
794
795        assert_ne!(generated_id, explicit_id);
796        assert!(scene.get_node(generated_id).is_some());
797    }
798}