Skip to main content

canvas_core/
schema.rs

1//! Canonical serialized representation for scenes shared across MCP, WebSocket, and web client.
2
3use serde::{Deserialize, Serialize};
4
5use crate::{Element, ElementId, ElementKind, Scene, Transform};
6
7/// Document-friendly element description.
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct ElementDocument {
10    /// Element identifier.
11    pub id: String,
12    /// Element content.
13    pub kind: ElementKind,
14    /// Transform metadata.
15    #[serde(default = "ElementDocument::default_transform")]
16    pub transform: Transform,
17    /// Interactivity flag.
18    #[serde(default = "ElementDocument::default_interactive")]
19    pub interactive: bool,
20    /// Selection flag.
21    #[serde(default)]
22    pub selected: bool,
23}
24
25impl From<&Element> for ElementDocument {
26    fn from(element: &Element) -> Self {
27        Self {
28            id: element.id.to_string(),
29            kind: element.kind.clone(),
30            transform: element.transform,
31            interactive: element.interactive,
32            selected: element.selected,
33        }
34    }
35}
36
37impl ElementDocument {
38    fn default_transform() -> Transform {
39        Transform::default()
40    }
41
42    const fn default_interactive() -> bool {
43        true
44    }
45
46    /// Convert document to runtime element.
47    ///
48    /// # Errors
49    ///
50    /// Returns error string if the element id is not a valid UUID.
51    pub fn into_element(self) -> Result<Element, String> {
52        let mut element = Element::new(self.kind).with_transform(self.transform);
53        element.interactive = self.interactive;
54        element.selected = self.selected;
55        let id = ElementId::parse(&self.id).map_err(|e| e.to_string())?;
56        element.id = id;
57        Ok(element)
58    }
59}
60
61/// Viewport information.
62#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
63pub struct ViewportDocument {
64    /// Width in pixels.
65    pub width: f32,
66    /// Height in pixels.
67    pub height: f32,
68    /// Zoom level.
69    #[serde(default = "ViewportDocument::default_zoom")]
70    pub zoom: f32,
71    /// Horizontal pan offset.
72    #[serde(default)]
73    pub pan_x: f32,
74    /// Vertical pan offset.
75    #[serde(default)]
76    pub pan_y: f32,
77}
78
79impl ViewportDocument {
80    const fn default_zoom() -> f32 {
81        1.0
82    }
83}
84
85impl From<&Scene> for ViewportDocument {
86    fn from(scene: &Scene) -> Self {
87        Self {
88            width: scene.viewport_width,
89            height: scene.viewport_height,
90            zoom: scene.zoom,
91            pan_x: scene.pan_x,
92            pan_y: scene.pan_y,
93        }
94    }
95}
96
97/// Canonical scene document.
98#[derive(Debug, Clone, Serialize, Deserialize)]
99pub struct SceneDocument {
100    /// Scene identifier/session.
101    pub session_id: String,
102    /// Viewport metadata.
103    pub viewport: ViewportDocument,
104    /// Elements in z-order order.
105    pub elements: Vec<ElementDocument>,
106    /// Timestamp in milliseconds.
107    pub timestamp: u64,
108}
109
110impl SceneDocument {
111    /// Build a document from a runtime scene.
112    pub fn from_scene(session_id: impl Into<String>, scene: &Scene, timestamp: u64) -> Self {
113        let mut elements: Vec<_> = scene.elements().map(ElementDocument::from).collect();
114        elements.sort_by_key(|doc| doc.transform.z_index);
115        Self {
116            session_id: session_id.into(),
117            viewport: ViewportDocument::from(scene),
118            elements,
119            timestamp,
120        }
121    }
122
123    /// Apply this document to a scene (overwriting current data).
124    ///
125    /// # Errors
126    ///
127    /// Returns error string if any element cannot be materialized.
128    pub fn into_scene(self) -> Result<Scene, String> {
129        let mut scene = Scene::new(self.viewport.width, self.viewport.height);
130        scene.zoom = self.viewport.zoom;
131        scene.pan_x = self.viewport.pan_x;
132        scene.pan_y = self.viewport.pan_y;
133
134        for element_doc in self.elements {
135            let element = element_doc.into_element()?;
136            scene.add_element(element);
137        }
138
139        Ok(scene)
140    }
141}