Skip to main content

qualia_core_db/domains/geospatial/
render_surface.rs

1//! Render surface descriptor for Chora canvas proxy (Phase 6).
2//!
3//! Exposes the WebGPU/canvas2d surface configuration that qApps use to
4//! initialise their renderer against the active world + temporal slice.
5
6use serde::{Deserialize, Serialize};
7
8/// Supported render backends for the Chora canvas proxy.
9pub const BACKEND_WEBGPU: &str = "webgpu";
10pub const BACKEND_CANVAS2D: &str = "canvas2d";
11
12/// Serializable surface configuration passed to the qApp renderer.
13#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
14pub struct RenderSurfaceDescriptor {
15    pub width: u32,
16    pub height: u32,
17    /// `"webgpu"` or `"canvas2d"`.
18    pub backend: String,
19    /// Current temporal scrub position (unix seconds).
20    pub temporal_t: u64,
21    /// Active canvas world identifier.
22    pub active_world_id: String,
23    /// Reference-frame origin latitude (degrees).
24    pub origin_lat: f64,
25    /// Reference-frame origin longitude (degrees).
26    pub origin_lon: f64,
27}
28
29/// Build a validated render surface descriptor from navigation + canvas state.
30pub fn build_surface_descriptor(
31    width: u32,
32    height: u32,
33    backend: &str,
34    temporal_t: u64,
35    active_world_id: &str,
36    origin_lat: f64,
37    origin_lon: f64,
38) -> Result<RenderSurfaceDescriptor, String> {
39    if width == 0 || height == 0 {
40        return Err("surface dimensions must be non-zero".into());
41    }
42    let backend = match backend {
43        BACKEND_WEBGPU | BACKEND_CANVAS2D => backend.to_string(),
44        other => {
45            return Err(format!(
46                "unsupported render backend '{other}'; expected webgpu or canvas2d"
47            ))
48        }
49    };
50    if active_world_id.trim().is_empty() {
51        return Err("active_world_id must not be empty".into());
52    }
53    Ok(RenderSurfaceDescriptor {
54        width,
55        height,
56        backend,
57        temporal_t,
58        active_world_id: active_world_id.to_string(),
59        origin_lat,
60        origin_lon,
61    })
62}
63
64#[cfg(test)]
65mod tests {
66    use super::*;
67
68    #[test]
69    fn build_surface_descriptor_roundtrip() {
70        let desc = build_surface_descriptor(
71            1280,
72            720,
73            BACKEND_WEBGPU,
74            1_750_000_000,
75            "q42:world:demo-offline",
76            -33.8688,
77            151.2093,
78        )
79        .unwrap();
80        assert_eq!(desc.width, 1280);
81        assert_eq!(desc.height, 720);
82        assert_eq!(desc.backend, BACKEND_WEBGPU);
83        assert_eq!(desc.temporal_t, 1_750_000_000);
84        assert_eq!(desc.active_world_id, "q42:world:demo-offline");
85        let json = serde_json::to_string(&desc).unwrap();
86        let parsed: RenderSurfaceDescriptor = serde_json::from_str(&json).unwrap();
87        assert_eq!(parsed, desc);
88    }
89
90    #[test]
91    fn rejects_invalid_backend() {
92        assert!(build_surface_descriptor(800, 600, "opengl", 0, "world", 0.0, 0.0).is_err());
93    }
94
95    #[test]
96    fn rejects_zero_dimensions() {
97        assert!(build_surface_descriptor(0, 600, BACKEND_CANVAS2D, 0, "world", 0.0, 0.0).is_err());
98    }
99}