Skip to main content

canvas_core/
element.rs

1//! Canvas elements - the building blocks of scenes.
2
3use serde::{Deserialize, Serialize};
4use uuid::Uuid;
5
6/// Unique identifier for an element.
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
8pub struct ElementId(Uuid);
9
10impl ElementId {
11    /// Create a new unique element ID.
12    #[must_use]
13    pub fn new() -> Self {
14        Self(Uuid::new_v4())
15    }
16
17    /// Create from an existing UUID.
18    #[must_use]
19    pub fn from_uuid(uuid: Uuid) -> Self {
20        Self(uuid)
21    }
22
23    /// Parse an element ID from a string.
24    ///
25    /// # Errors
26    ///
27    /// Returns an error if the string is not a valid UUID.
28    pub fn parse(s: &str) -> Result<Self, uuid::Error> {
29        Uuid::parse_str(s).map(Self)
30    }
31}
32
33impl Default for ElementId {
34    fn default() -> Self {
35        Self::new()
36    }
37}
38
39impl std::fmt::Display for ElementId {
40    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
41        write!(f, "{}", self.0)
42    }
43}
44
45/// The type of content an element contains.
46#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
47#[serde(tag = "type", content = "data")]
48pub enum ElementKind {
49    /// A 2D chart (bar, line, pie, etc.).
50    Chart {
51        /// Chart type identifier.
52        chart_type: String,
53        /// Chart data as JSON.
54        data: serde_json::Value,
55    },
56
57    /// A 2D image (PNG, JPG, SVG).
58    Image {
59        /// Image source URI or base64 data.
60        src: String,
61        /// Image format.
62        format: ImageFormat,
63    },
64
65    /// A 3D model (glTF).
66    Model3D {
67        /// glTF source URI.
68        src: String,
69        /// Initial rotation (euler angles in radians).
70        rotation: [f32; 3],
71        /// Initial scale.
72        scale: f32,
73    },
74
75    /// A video stream or WebRTC feed.
76    Video {
77        /// Stream identifier (peer ID, "local", or media URL).
78        stream_id: String,
79        /// Whether this is a live WebRTC stream.
80        is_live: bool,
81        /// Whether to mirror the video (useful for local camera).
82        mirror: bool,
83        /// Optional crop region within the video frame.
84        crop: Option<CropRect>,
85        /// Optional media quality configuration.
86        media_config: Option<MediaConfig>,
87    },
88
89    /// A transparent overlay layer for annotations on top of video.
90    OverlayLayer {
91        /// Child element IDs drawn on this layer.
92        children: Vec<ElementId>,
93        /// Background opacity (0.0 = fully transparent).
94        opacity: f32,
95    },
96
97    /// A text label or annotation.
98    Text {
99        /// Text content.
100        content: String,
101        /// Font size in pixels.
102        font_size: f32,
103        /// Text color as hex.
104        color: String,
105    },
106
107    /// A container group for other elements.
108    Group {
109        /// Child element IDs.
110        children: Vec<ElementId>,
111    },
112}
113
114/// Supported image formats.
115#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
116#[serde(rename_all = "lowercase")]
117pub enum ImageFormat {
118    /// PNG image.
119    Png,
120    /// JPEG image.
121    Jpeg,
122    /// SVG vector image.
123    Svg,
124    /// WebP image.
125    WebP,
126}
127
128/// A crop rectangle for video frames.
129/// Values are normalized (0.0 to 1.0) relative to the video dimensions.
130#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
131pub struct CropRect {
132    /// Left edge (0.0 = leftmost).
133    pub x: f32,
134    /// Top edge (0.0 = topmost).
135    pub y: f32,
136    /// Width (1.0 = full width).
137    pub width: f32,
138    /// Height (1.0 = full height).
139    pub height: f32,
140}
141
142impl Default for CropRect {
143    fn default() -> Self {
144        Self {
145            x: 0.0,
146            y: 0.0,
147            width: 1.0,
148            height: 1.0,
149        }
150    }
151}
152
153impl CropRect {
154    /// Create a crop rect that shows the full frame.
155    #[must_use]
156    pub fn full() -> Self {
157        Self::default()
158    }
159
160    /// Create a centered square crop (useful for profile pictures).
161    #[must_use]
162    pub fn center_square() -> Self {
163        Self {
164            x: 0.25,
165            y: 0.0,
166            width: 0.5,
167            height: 1.0,
168        }
169    }
170}
171
172/// Video resolution presets.
173#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
174#[serde(rename_all = "lowercase")]
175pub enum Resolution {
176    /// 426x240 (very low bandwidth).
177    R240p,
178    /// 640x360.
179    R360p,
180    /// 854x480.
181    R480p,
182    /// 1280x720 (default).
183    #[default]
184    R720p,
185    /// 1920x1080.
186    R1080p,
187}
188
189impl Resolution {
190    /// Get width x height dimensions for this resolution.
191    #[must_use]
192    pub const fn dimensions(&self) -> (u32, u32) {
193        match self {
194            Self::R240p => (426, 240),
195            Self::R360p => (640, 360),
196            Self::R480p => (854, 480),
197            Self::R720p => (1280, 720),
198            Self::R1080p => (1920, 1080),
199        }
200    }
201
202    /// Get suggested bitrate in kbps for this resolution.
203    #[must_use]
204    pub const fn suggested_bitrate_kbps(&self) -> u32 {
205        match self {
206            Self::R240p => 400,
207            Self::R360p => 800,
208            Self::R480p => 1200,
209            Self::R720p => 2500,
210            Self::R1080p => 5000,
211        }
212    }
213}
214
215/// Quality presets for automatic video configuration.
216#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
217#[serde(rename_all = "lowercase")]
218pub enum QualityPreset {
219    /// Automatic adaptation based on network conditions.
220    #[default]
221    Auto,
222    /// Low bandwidth mode (240p-360p, ~400kbps).
223    Low,
224    /// Medium quality (480p, ~1200kbps).
225    Medium,
226    /// High quality (720p, ~2500kbps).
227    High,
228    /// Maximum quality (1080p, ~5000kbps).
229    Ultra,
230}
231
232impl QualityPreset {
233    /// Get the target resolution for this preset.
234    #[must_use]
235    pub const fn resolution(&self) -> Resolution {
236        match self {
237            Self::Auto | Self::High => Resolution::R720p,
238            Self::Low => Resolution::R360p,
239            Self::Medium => Resolution::R480p,
240            Self::Ultra => Resolution::R1080p,
241        }
242    }
243
244    /// Get the target bitrate in kbps for this preset.
245    #[must_use]
246    pub const fn bitrate_kbps(&self) -> u32 {
247        match self {
248            Self::Low => 400,
249            Self::Medium => 1200,
250            Self::Auto | Self::High => 2500,
251            Self::Ultra => 5000,
252        }
253    }
254
255    /// Get the target framerate for this preset.
256    #[must_use]
257    pub const fn framerate(&self) -> u8 {
258        match self {
259            Self::Low => 15,
260            Self::Medium => 24,
261            Self::Auto | Self::High | Self::Ultra => 30,
262        }
263    }
264}
265
266/// Configuration for video stream quality.
267#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
268pub struct MediaConfig {
269    /// Target bitrate in kbps (e.g., 1500 for 720p).
270    pub bitrate_kbps: Option<u32>,
271    /// Maximum resolution constraint.
272    pub max_resolution: Option<Resolution>,
273    /// Quality preset (overrides specific settings when not Auto).
274    pub quality_preset: QualityPreset,
275    /// Target framerate (default 30).
276    pub target_fps: Option<u8>,
277    /// Whether audio track is enabled.
278    pub audio_enabled: bool,
279}
280
281impl Default for MediaConfig {
282    fn default() -> Self {
283        Self {
284            bitrate_kbps: None,
285            max_resolution: None,
286            quality_preset: QualityPreset::Auto,
287            target_fps: None,
288            audio_enabled: false,
289        }
290    }
291}
292
293impl MediaConfig {
294    /// Create a config from a quality preset.
295    #[must_use]
296    pub fn from_preset(preset: QualityPreset) -> Self {
297        Self {
298            bitrate_kbps: Some(preset.bitrate_kbps()),
299            max_resolution: Some(preset.resolution()),
300            quality_preset: preset,
301            target_fps: Some(preset.framerate()),
302            audio_enabled: false,
303        }
304    }
305
306    /// Get the effective bitrate, considering preset.
307    #[must_use]
308    pub fn effective_bitrate_kbps(&self) -> u32 {
309        self.bitrate_kbps
310            .unwrap_or_else(|| self.quality_preset.bitrate_kbps())
311    }
312
313    /// Get the effective resolution, considering preset.
314    #[must_use]
315    pub fn effective_resolution(&self) -> Resolution {
316        self.max_resolution
317            .unwrap_or_else(|| self.quality_preset.resolution())
318    }
319
320    /// Get the effective framerate, considering preset.
321    #[must_use]
322    pub fn effective_fps(&self) -> u8 {
323        self.target_fps
324            .unwrap_or_else(|| self.quality_preset.framerate())
325    }
326}
327
328/// Real-time media statistics from WebRTC.
329#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
330pub struct MediaStats {
331    /// Round-trip time in milliseconds.
332    pub rtt_ms: Option<f64>,
333    /// Jitter in milliseconds.
334    pub jitter_ms: Option<f64>,
335    /// Packet loss percentage (0.0 - 100.0).
336    pub packet_loss_percent: Option<f64>,
337    /// Current framerate.
338    pub fps: Option<f64>,
339    /// Current bitrate in kbps.
340    pub bitrate_kbps: Option<f64>,
341    /// Timestamp of last update (unix milliseconds).
342    pub timestamp_ms: u64,
343}
344
345impl MediaStats {
346    /// Check if the connection quality is good based on stats.
347    #[must_use]
348    pub fn is_quality_good(&self) -> bool {
349        let loss_ok = self.packet_loss_percent.is_none_or(|l| l < 2.0);
350        let rtt_ok = self.rtt_ms.is_none_or(|r| r < 150.0);
351        loss_ok && rtt_ok
352    }
353
354    /// Check if adaptive quality should downgrade.
355    #[must_use]
356    pub fn should_downgrade(&self) -> bool {
357        let high_loss = self.packet_loss_percent.is_some_and(|l| l > 5.0);
358        let high_rtt = self.rtt_ms.is_some_and(|r| r > 300.0);
359        high_loss || high_rtt
360    }
361}
362
363/// Transform for positioning and sizing elements.
364#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
365pub struct Transform {
366    /// X position (pixels from left).
367    pub x: f32,
368    /// Y position (pixels from top).
369    pub y: f32,
370    /// Width in pixels.
371    pub width: f32,
372    /// Height in pixels.
373    pub height: f32,
374    /// Rotation in radians.
375    pub rotation: f32,
376    /// Z-index for layering.
377    pub z_index: i32,
378}
379
380impl Default for Transform {
381    fn default() -> Self {
382        Self {
383            x: 0.0,
384            y: 0.0,
385            width: 100.0,
386            height: 100.0,
387            rotation: 0.0,
388            z_index: 0,
389        }
390    }
391}
392
393/// A canvas element with content and transform.
394#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
395pub struct Element {
396    /// Unique identifier.
397    pub id: ElementId,
398    /// Element content type.
399    pub kind: ElementKind,
400    /// Position and size.
401    pub transform: Transform,
402    /// Whether this element is selected.
403    pub selected: bool,
404    /// Whether this element can be interacted with.
405    pub interactive: bool,
406    /// Optional parent element ID (for grouped elements).
407    pub parent: Option<ElementId>,
408}
409
410impl Element {
411    /// Create a new element with the given kind.
412    #[must_use]
413    pub fn new(kind: ElementKind) -> Self {
414        Self {
415            id: ElementId::new(),
416            kind,
417            transform: Transform::default(),
418            selected: false,
419            interactive: true,
420            parent: None,
421        }
422    }
423
424    /// Set the transform.
425    #[must_use]
426    pub fn with_transform(mut self, transform: Transform) -> Self {
427        self.transform = transform;
428        self
429    }
430
431    /// Set whether the element is interactive.
432    #[must_use]
433    pub fn with_interactive(mut self, interactive: bool) -> Self {
434        self.interactive = interactive;
435        self
436    }
437
438    /// Check if a point (in canvas coordinates) is within this element.
439    #[must_use]
440    pub fn contains_point(&self, x: f32, y: f32) -> bool {
441        let t = &self.transform;
442        x >= t.x && x <= t.x + t.width && y >= t.y && y <= t.y + t.height
443    }
444}