Skip to main content

oxihuman_viewer/
render_loop.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3
4//! Main viewer struct and render loop.
5
6use crate::camera::CameraState;
7use crate::gpu::MeshUploadBuffer;
8use crate::scene_state::{ViewerConfig, ViewerStats};
9
10// ── Viewer ────────────────────────────────────────────────────────────────────
11
12/// Stub viewer -- will be replaced by wgpu surface in Phase 2.
13pub struct Viewer {
14    config: ViewerConfig,
15    pub camera: CameraState,
16    pub current_mesh: Option<MeshUploadBuffer>,
17    frame_count: u64,
18}
19
20impl Viewer {
21    pub fn new(config: ViewerConfig) -> Self {
22        Viewer {
23            config,
24            camera: CameraState::default(),
25            current_mesh: None,
26            frame_count: 0,
27        }
28    }
29
30    /// Upload a mesh buffer to the viewer (CPU-side store; GPU upload in Phase 2).
31    pub fn upload_mesh(&mut self, buf: MeshUploadBuffer) {
32        self.current_mesh = Some(buf);
33    }
34
35    /// Simulate a render tick.  Returns [`ViewerStats`] for the current frame.
36    pub fn render_frame(&mut self) -> ViewerStats {
37        self.frame_count += 1;
38        let (verts, tris) = self
39            .current_mesh
40            .as_ref()
41            .map(|m| (m.positions.len(), m.indices.len() / 3))
42            .unwrap_or((0, 0));
43        ViewerStats {
44            frame_count: self.frame_count,
45            vertex_count: verts,
46            triangle_count: tris,
47        }
48    }
49
50    /// Return a reference to the current camera state.
51    pub fn get_camera(&self) -> &CameraState {
52        &self.camera
53    }
54
55    /// Orbit the camera around its target.
56    pub fn orbit_camera(&mut self, yaw: f32, pitch: f32) {
57        self.camera.orbit(yaw, pitch);
58    }
59
60    /// Return a reference to the viewer configuration.
61    pub fn config(&self) -> &ViewerConfig {
62        &self.config
63    }
64}
65
66// ── Tests ─────────────────────────────────────────────────────────────────────
67
68#[cfg(test)]
69mod tests {
70    use super::*;
71
72    #[test]
73    fn viewer_render_frame_returns_stats() {
74        let mut v = Viewer::new(ViewerConfig::default());
75        let stats = v.render_frame();
76        assert_eq!(stats.frame_count, 1);
77        assert_eq!(stats.vertex_count, 0);
78        assert_eq!(stats.triangle_count, 0);
79    }
80
81    #[test]
82    fn viewer_upload_mesh_reflected_in_stats() {
83        let buf = MeshUploadBuffer {
84            positions: vec![[0.0; 3], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]],
85            normals: vec![[0.0, 0.0, 1.0]; 3],
86            uvs: vec![[0.0; 2]; 3],
87            indices: vec![0, 1, 2],
88            timestamp: 0,
89        };
90        let mut v = Viewer::new(ViewerConfig::default());
91        v.upload_mesh(buf);
92        let stats = v.render_frame();
93        assert_eq!(stats.vertex_count, 3);
94        assert_eq!(stats.triangle_count, 1);
95    }
96
97    #[test]
98    fn viewer_orbit_camera_delegates() {
99        let mut v = Viewer::new(ViewerConfig::default());
100        let before = v.get_camera().position;
101        v.orbit_camera(30.0, 0.0);
102        assert_ne!(v.get_camera().position, before);
103    }
104
105    #[test]
106    fn viewer_frame_count_increments() {
107        let mut v = Viewer::new(ViewerConfig::default());
108        v.render_frame();
109        v.render_frame();
110        let s = v.render_frame();
111        assert_eq!(s.frame_count, 3);
112    }
113}