Skip to main content

concinnity_core/components/
camera3d.rs

1// src/components/camera3d.rs
2//
3// Runtime 3D camera component. Its authored args and controller config live in
4// the schema crate (concinnity_asset::camera3d).
5
6use concinnity_asset::cook;
7
8use crate::components::CameraController;
9use crate::ecs::Component;
10
11/// Declares the 3D camera. One per scene.
12#[derive(Debug, serde::Serialize, serde::Deserialize)]
13pub struct Camera3D {
14    /// Vertical field of view in degrees.
15    pub fov_y_degrees: f32,
16    /// Near clip distance in world units.
17    pub near: f32,
18    /// Far clip distance in world units.
19    pub far: f32,
20    /// Current view matrix, written each step by the active camera system.
21    /// Column-major, matching the GLSL mat4 convention.
22    pub view_matrix: [[f32; 4]; 4],
23    /// Current world-space eye position, kept in sync with view_matrix.
24    pub position: [f32; 3],
25    /// Current yaw in radians.
26    pub yaw: f32,
27    /// Current pitch in radians.
28    pub pitch: f32,
29    /// World-space horizontal movement intent (units/second). Written by
30    /// Camera3DSystem each frame, consumed by PhysicsSystem. Runtime-only.
31    pub desired_move: [f32; 3],
32    /// Set for one frame when the jump key is pressed. Runtime-only.
33    pub jump_requested: bool,
34    /// Set for one frame when the interact key is pressed. Runtime-only.
35    pub interact_requested: bool,
36    /// Controller settings, or `None` for an uncontrolled (cutscene) camera.
37    /// Read once by the internal camera controller at init.
38    pub controller: Option<CameraController>,
39}
40
41impl Camera3D {
42    /// Translate the authored args into the runtime camera: compose the initial
43    /// view matrix and zero the runtime state. Run by cook at build time (the
44    /// baked blob record carries the result) and by tests that need a camera.
45    pub fn bake(args: cook::Camera3D) -> Self {
46        Self {
47            fov_y_degrees: args.fov_y_degrees,
48            near: args.near,
49            far: args.far,
50            view_matrix: crate::gfx::camera::view_matrix(args.position, args.yaw, args.pitch),
51            position: args.position,
52            yaw: args.yaw,
53            pitch: args.pitch,
54            desired_move: [0.0; 3],
55            jump_requested: false,
56            interact_requested: false,
57            controller: args.controller,
58        }
59    }
60}
61
62impl Component for Camera3D {
63    const NAME: &'static str = "Camera3D";
64
65    fn from_baked(bytes: &[u8]) -> Result<Self, crate::result::CnResult> {
66        Ok(crate::blob::decode_exact(bytes)?)
67    }
68}
69
70#[cfg(test)]
71mod tests {
72    use super::*;
73    use crate::components::FollowDrive;
74    use crate::ecs::SkinnedMeshHandle;
75
76    #[test]
77    fn follow_block_deserializes_names_and_defaults() {
78        crate::test_support::reset_interner();
79        crate::test_support::intern_all(&["hero"]);
80        let args: cook::Camera3D = serde_json::from_value(serde_json::json!({
81            "controller": {"follow": {"target": "hero", "drive": "direct"}}
82        }))
83        .unwrap();
84        let follow = args.controller.unwrap().follow.unwrap();
85        // "hero" interns to id 0, and with no SkinnedMesh handle resolver installed
86        // the reference falls back to that interned value as its handle.
87        assert_eq!(follow.target, Some(SkinnedMeshHandle(0)));
88        assert_eq!(follow.drive, FollowDrive::Direct);
89        // Omitted fields keep the documented defaults.
90        assert_eq!(follow.speed_parameter, "speed");
91        assert!((follow.distance - 4.0).abs() < 1e-6);
92        assert!((follow.height - 1.5).abs() < 1e-6);
93        assert_eq!(follow.jump_height, 0.0);
94
95        // No follow block keeps the first-person modes.
96        let bare: cook::Camera3D =
97            serde_json::from_value(serde_json::json!({"controller": {}})).unwrap();
98        assert!(bare.controller.unwrap().follow.is_none());
99    }
100}