Skip to main content

communitas_ui_api/
canvas.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! Canvas DTOs for collaborative drawing surfaces.
4
5use serde::{Deserialize, Serialize};
6
7/// Information about a canvas.
8#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
9pub struct CanvasInfo {
10    /// Unique canvas identifier.
11    pub id: String,
12    /// Entity this canvas belongs to.
13    pub entity_id: String,
14    /// Canvas display name.
15    pub name: String,
16    /// Canvas width in pixels.
17    pub width: u32,
18    /// Canvas height in pixels.
19    pub height: u32,
20    /// Creation timestamp (Unix epoch ms).
21    pub created_at: i64,
22    /// Last update timestamp (Unix epoch ms).
23    pub updated_at: i64,
24}
25
26/// A 2D point with optional pressure data.
27#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
28pub struct Point {
29    /// X coordinate.
30    pub x: f32,
31    /// Y coordinate.
32    pub y: f32,
33    /// Optional pressure value (0.0 to 1.0) for pen/stylus input.
34    pub pressure: Option<f32>,
35}
36
37/// Transform applied to canvas elements.
38#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
39pub struct Transform {
40    /// X translation offset.
41    pub translate_x: f32,
42    /// Y translation offset.
43    pub translate_y: f32,
44    /// X scale factor.
45    pub scale_x: f32,
46    /// Y scale factor.
47    pub scale_y: f32,
48    /// Rotation angle in radians.
49    pub rotation: f32,
50}
51
52impl Default for Transform {
53    fn default() -> Self {
54        Self {
55            translate_x: 0.0,
56            translate_y: 0.0,
57            scale_x: 1.0,
58            scale_y: 1.0,
59            rotation: 0.0,
60        }
61    }
62}
63
64/// Types of canvas elements.
65#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
66pub enum ElementType {
67    /// Freehand path/stroke.
68    Path {
69        /// Points along the path.
70        points: Vec<Point>,
71        /// Stroke width in pixels.
72        stroke_width: f32,
73        /// Stroke color (CSS color string).
74        color: String,
75    },
76    /// Rectangle shape.
77    Rectangle {
78        /// X position of top-left corner.
79        x: f32,
80        /// Y position of top-left corner.
81        y: f32,
82        /// Width of rectangle.
83        width: f32,
84        /// Height of rectangle.
85        height: f32,
86        /// Fill color (CSS color string).
87        fill: Option<String>,
88        /// Stroke color (CSS color string).
89        stroke: Option<String>,
90    },
91    /// Ellipse shape.
92    Ellipse {
93        /// Center X coordinate.
94        cx: f32,
95        /// Center Y coordinate.
96        cy: f32,
97        /// X radius.
98        rx: f32,
99        /// Y radius.
100        ry: f32,
101        /// Fill color (CSS color string).
102        fill: Option<String>,
103        /// Stroke color (CSS color string).
104        stroke: Option<String>,
105    },
106    /// Text element.
107    Text {
108        /// X position.
109        x: f32,
110        /// Y position.
111        y: f32,
112        /// Text content.
113        content: String,
114        /// Font size in pixels.
115        font_size: f32,
116        /// Text color (CSS color string).
117        color: String,
118    },
119    /// Image element.
120    Image {
121        /// X position.
122        x: f32,
123        /// Y position.
124        y: f32,
125        /// Image width.
126        width: f32,
127        /// Image height.
128        height: f32,
129        /// Base64 data URL or external URL.
130        data_url: String,
131    },
132}
133
134/// A canvas element with metadata.
135#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
136pub struct CanvasElement {
137    /// Unique element identifier.
138    pub id: String,
139    /// Element type and data.
140    pub element_type: ElementType,
141    /// Layer this element belongs to.
142    pub layer_id: String,
143    /// Z-order within the layer.
144    pub z_index: i32,
145    /// Element opacity (0.0 to 1.0).
146    pub opacity: f32,
147    /// Optional transform applied to the element.
148    pub transform: Option<Transform>,
149    /// User ID of the creator.
150    pub created_by: String,
151    /// Creation timestamp (Unix epoch ms).
152    pub created_at: i64,
153}
154
155/// A canvas layer.
156#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
157pub struct Layer {
158    /// Unique layer identifier.
159    pub id: String,
160    /// Layer display name.
161    pub name: String,
162    /// Whether the layer is visible.
163    pub visible: bool,
164    /// Whether the layer is locked for editing.
165    pub locked: bool,
166    /// Layer opacity (0.0 to 1.0).
167    pub opacity: f32,
168    /// Z-order of the layer.
169    pub z_index: i32,
170}
171
172/// Complete canvas state for UI rendering.
173#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
174pub struct CanvasState {
175    /// Canvas metadata.
176    pub canvas_info: CanvasInfo,
177    /// All elements on the canvas.
178    pub elements: Vec<CanvasElement>,
179    /// All layers.
180    pub layers: Vec<Layer>,
181    /// Currently active layer ID.
182    pub active_layer_id: String,
183}
184
185impl Default for CanvasState {
186    fn default() -> Self {
187        Self {
188            canvas_info: CanvasInfo {
189                id: String::new(),
190                entity_id: String::new(),
191                name: String::new(),
192                width: 1920,
193                height: 1080,
194                created_at: 0,
195                updated_at: 0,
196            },
197            elements: Vec::new(),
198            layers: Vec::new(),
199            active_layer_id: String::new(),
200        }
201    }
202}
203
204/// Remote user's cursor position for collaborative editing.
205#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
206pub struct RemoteCursor {
207    /// User ID of the cursor owner.
208    pub user_id: String,
209    /// User's display name.
210    pub user_name: String,
211    /// Cursor X position on canvas.
212    pub x: f32,
213    /// Cursor Y position on canvas.
214    pub y: f32,
215    /// User's assigned color (CSS color string).
216    pub color: String,
217    /// Last activity timestamp (Unix epoch ms).
218    pub last_active: i64,
219    /// Currently selected tool.
220    pub tool: Option<String>,
221}
222
223/// Types of actions recorded in history.
224#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
225pub enum HistoryActionType {
226    /// Element added.
227    AddElement,
228    /// Element deleted.
229    DeleteElement,
230    /// Element modified.
231    ModifyElement,
232    /// Layer added.
233    AddLayer,
234    /// Layer deleted.
235    DeleteLayer,
236    /// Layer modified.
237    ModifyLayer,
238    /// Multiple operations grouped.
239    BatchOperation,
240}
241
242impl HistoryActionType {
243    /// Human-readable label for the action type.
244    pub fn label(&self) -> &'static str {
245        match self {
246            Self::AddElement => "Add Element",
247            Self::DeleteElement => "Delete Element",
248            Self::ModifyElement => "Modify Element",
249            Self::AddLayer => "Add Layer",
250            Self::DeleteLayer => "Delete Layer",
251            Self::ModifyLayer => "Modify Layer",
252            Self::BatchOperation => "Batch Operation",
253        }
254    }
255}
256
257/// A history entry for undo/redo.
258#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
259pub struct HistoryEntry {
260    /// Unique entry identifier.
261    pub id: String,
262    /// Type of action performed.
263    pub action_type: HistoryActionType,
264    /// Timestamp of the action (Unix epoch ms).
265    pub timestamp: i64,
266    /// User who performed the action.
267    pub user_id: String,
268    /// Human-readable description.
269    pub description: String,
270    /// Whether undo is available from this point.
271    pub can_undo: bool,
272    /// Whether redo is available from this point.
273    pub can_redo: bool,
274}
275
276/// Types of operations for offline queue.
277#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
278pub enum OperationType {
279    /// Add new element.
280    AddElement,
281    /// Update existing element.
282    UpdateElement,
283    /// Delete element.
284    DeleteElement,
285    /// Add new layer.
286    AddLayer,
287    /// Update existing layer.
288    UpdateLayer,
289    /// Delete layer.
290    DeleteLayer,
291}
292
293impl OperationType {
294    /// Human-readable label for the operation type.
295    pub fn label(&self) -> &'static str {
296        match self {
297            Self::AddElement => "Add Element",
298            Self::UpdateElement => "Update Element",
299            Self::DeleteElement => "Delete Element",
300            Self::AddLayer => "Add Layer",
301            Self::UpdateLayer => "Update Layer",
302            Self::DeleteLayer => "Delete Layer",
303        }
304    }
305}
306
307/// Status of an offline operation.
308#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
309pub enum OfflineStatus {
310    /// Waiting to sync.
311    Pending,
312    /// Currently syncing.
313    Syncing,
314    /// Successfully synced.
315    Synced,
316    /// Failed to sync with error message.
317    Failed(String),
318}
319
320impl OfflineStatus {
321    /// Whether the operation is still pending.
322    pub fn is_pending(&self) -> bool {
323        matches!(self, Self::Pending)
324    }
325
326    /// Whether the operation is currently syncing.
327    pub fn is_syncing(&self) -> bool {
328        matches!(self, Self::Syncing)
329    }
330
331    /// Whether the operation has been synced.
332    pub fn is_synced(&self) -> bool {
333        matches!(self, Self::Synced)
334    }
335
336    /// Whether the operation has failed.
337    pub fn is_failed(&self) -> bool {
338        matches!(self, Self::Failed(_))
339    }
340}
341
342/// High-level sync state for UI display.
343#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
344pub enum SyncState {
345    /// All changes synced to network.
346    #[default]
347    Synced,
348    /// Currently syncing changes.
349    Syncing,
350    /// Changes pending sync (offline).
351    Pending,
352    /// Sync failed with errors.
353    Error,
354}
355
356/// An operation queued for offline sync.
357#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
358pub struct OfflineOperation {
359    /// Unique operation identifier.
360    pub id: String,
361    /// Type of operation.
362    pub operation_type: OperationType,
363    /// JSON-serialized payload.
364    pub payload: String,
365    /// Creation timestamp (Unix epoch ms).
366    pub created_at: i64,
367    /// Number of sync retry attempts.
368    pub retry_count: u32,
369    /// Current sync status.
370    pub status: OfflineStatus,
371}
372
373/// Tool types for the canvas toolbar.
374#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
375pub enum ToolType {
376    /// Selection tool.
377    #[default]
378    Select,
379    /// Pen for freehand drawing.
380    Pen,
381    /// Brush for softer strokes.
382    Brush,
383    /// Eraser tool.
384    Eraser,
385    /// Rectangle shape tool.
386    Rectangle,
387    /// Ellipse shape tool.
388    Ellipse,
389    /// Text tool.
390    Text,
391    /// Pan/drag viewport.
392    Pan,
393    /// Zoom tool.
394    Zoom,
395}
396
397impl ToolType {
398    /// Human-readable label for the tool.
399    pub fn label(&self) -> &'static str {
400        match self {
401            Self::Select => "Select",
402            Self::Pen => "Pen",
403            Self::Brush => "Brush",
404            Self::Eraser => "Eraser",
405            Self::Rectangle => "Rectangle",
406            Self::Ellipse => "Ellipse",
407            Self::Text => "Text",
408            Self::Pan => "Pan",
409            Self::Zoom => "Zoom",
410        }
411    }
412
413    /// Icon representation for the tool.
414    pub fn icon(&self) -> &'static str {
415        match self {
416            Self::Select => "pointer",
417            Self::Pen => "pen",
418            Self::Brush => "brush",
419            Self::Eraser => "eraser",
420            Self::Rectangle => "square",
421            Self::Ellipse => "circle",
422            Self::Text => "type",
423            Self::Pan => "move",
424            Self::Zoom => "zoom-in",
425        }
426    }
427
428    /// All available tools.
429    pub fn all() -> &'static [ToolType] {
430        &[
431            Self::Select,
432            Self::Pen,
433            Self::Brush,
434            Self::Eraser,
435            Self::Rectangle,
436            Self::Ellipse,
437            Self::Text,
438            Self::Pan,
439            Self::Zoom,
440        ]
441    }
442}
443
444/// Snapshot of canvas state including collaboration data.
445#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
446pub struct CanvasSnapshot {
447    /// Current canvas state.
448    pub state: CanvasState,
449    /// Remote cursors from other users.
450    pub remote_cursors: Vec<RemoteCursor>,
451    /// Recent history entries.
452    pub history: Vec<HistoryEntry>,
453    /// Pending offline operations.
454    pub offline_operations: Vec<OfflineOperation>,
455    /// Currently selected tool.
456    pub selected_tool: ToolType,
457    /// Current stroke color.
458    pub stroke_color: String,
459    /// Current fill color.
460    pub fill_color: Option<String>,
461    /// Current stroke width.
462    pub stroke_width: f32,
463}
464
465#[cfg(test)]
466mod tests {
467    use super::*;
468
469    #[test]
470    fn transform_default_identity() {
471        let t = Transform::default();
472        assert_eq!(t.translate_x, 0.0);
473        assert_eq!(t.translate_y, 0.0);
474        assert_eq!(t.scale_x, 1.0);
475        assert_eq!(t.scale_y, 1.0);
476        assert_eq!(t.rotation, 0.0);
477    }
478
479    #[test]
480    fn tool_type_labels() {
481        assert_eq!(ToolType::Select.label(), "Select");
482        assert_eq!(ToolType::Pen.label(), "Pen");
483        assert_eq!(ToolType::Brush.label(), "Brush");
484        assert_eq!(ToolType::Eraser.label(), "Eraser");
485        assert_eq!(ToolType::Rectangle.label(), "Rectangle");
486        assert_eq!(ToolType::Ellipse.label(), "Ellipse");
487        assert_eq!(ToolType::Text.label(), "Text");
488        assert_eq!(ToolType::Pan.label(), "Pan");
489        assert_eq!(ToolType::Zoom.label(), "Zoom");
490    }
491
492    #[test]
493    fn tool_type_icons() {
494        assert_eq!(ToolType::Select.icon(), "pointer");
495        assert_eq!(ToolType::Pen.icon(), "pen");
496        assert_eq!(ToolType::Eraser.icon(), "eraser");
497    }
498
499    #[test]
500    fn tool_type_all_variants() {
501        let all = ToolType::all();
502        assert_eq!(all.len(), 9);
503        assert!(all.contains(&ToolType::Select));
504        assert!(all.contains(&ToolType::Zoom));
505    }
506
507    #[test]
508    fn history_action_type_labels() {
509        assert_eq!(HistoryActionType::AddElement.label(), "Add Element");
510        assert_eq!(HistoryActionType::DeleteElement.label(), "Delete Element");
511        assert_eq!(HistoryActionType::ModifyElement.label(), "Modify Element");
512        assert_eq!(HistoryActionType::AddLayer.label(), "Add Layer");
513        assert_eq!(HistoryActionType::DeleteLayer.label(), "Delete Layer");
514        assert_eq!(HistoryActionType::ModifyLayer.label(), "Modify Layer");
515        assert_eq!(HistoryActionType::BatchOperation.label(), "Batch Operation");
516    }
517
518    #[test]
519    fn operation_type_labels() {
520        assert_eq!(OperationType::AddElement.label(), "Add Element");
521        assert_eq!(OperationType::UpdateElement.label(), "Update Element");
522        assert_eq!(OperationType::DeleteElement.label(), "Delete Element");
523        assert_eq!(OperationType::AddLayer.label(), "Add Layer");
524        assert_eq!(OperationType::UpdateLayer.label(), "Update Layer");
525        assert_eq!(OperationType::DeleteLayer.label(), "Delete Layer");
526    }
527
528    #[test]
529    fn offline_status_predicates() {
530        assert!(OfflineStatus::Pending.is_pending());
531        assert!(!OfflineStatus::Pending.is_syncing());
532
533        assert!(OfflineStatus::Syncing.is_syncing());
534        assert!(!OfflineStatus::Syncing.is_pending());
535
536        assert!(OfflineStatus::Synced.is_synced());
537        assert!(!OfflineStatus::Synced.is_failed());
538
539        let failed = OfflineStatus::Failed("error".to_string());
540        assert!(failed.is_failed());
541        assert!(!failed.is_synced());
542    }
543
544    #[test]
545    fn canvas_state_default() {
546        let state = CanvasState::default();
547        assert!(state.elements.is_empty());
548        assert!(state.layers.is_empty());
549        assert!(state.active_layer_id.is_empty());
550        assert_eq!(state.canvas_info.width, 1920);
551        assert_eq!(state.canvas_info.height, 1080);
552    }
553
554    #[test]
555    fn point_serialization() {
556        let point = Point {
557            x: 10.5,
558            y: 20.3,
559            pressure: Some(0.8),
560        };
561        let json = serde_json::to_string(&point).unwrap();
562        let deserialized: Point = serde_json::from_str(&json).unwrap();
563        assert_eq!(point, deserialized);
564    }
565
566    #[test]
567    fn element_type_path() {
568        let path = ElementType::Path {
569            points: vec![
570                Point {
571                    x: 0.0,
572                    y: 0.0,
573                    pressure: None,
574                },
575                Point {
576                    x: 100.0,
577                    y: 100.0,
578                    pressure: Some(0.5),
579                },
580            ],
581            stroke_width: 2.0,
582            color: "#000000".to_string(),
583        };
584        if let ElementType::Path {
585            points,
586            stroke_width,
587            ..
588        } = path
589        {
590            assert_eq!(points.len(), 2);
591            assert_eq!(stroke_width, 2.0);
592        } else {
593            panic!("Expected Path variant");
594        }
595    }
596
597    #[test]
598    fn layer_creation() {
599        let layer = Layer {
600            id: "layer-1".to_string(),
601            name: "Background".to_string(),
602            visible: true,
603            locked: false,
604            opacity: 1.0,
605            z_index: 0,
606        };
607        assert!(layer.visible);
608        assert!(!layer.locked);
609        assert_eq!(layer.opacity, 1.0);
610    }
611}