1use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
9pub struct CanvasInfo {
10 pub id: String,
12 pub entity_id: String,
14 pub name: String,
16 pub width: u32,
18 pub height: u32,
20 pub created_at: i64,
22 pub updated_at: i64,
24}
25
26#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
28pub struct Point {
29 pub x: f32,
31 pub y: f32,
33 pub pressure: Option<f32>,
35}
36
37#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
39pub struct Transform {
40 pub translate_x: f32,
42 pub translate_y: f32,
44 pub scale_x: f32,
46 pub scale_y: f32,
48 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#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
66pub enum ElementType {
67 Path {
69 points: Vec<Point>,
71 stroke_width: f32,
73 color: String,
75 },
76 Rectangle {
78 x: f32,
80 y: f32,
82 width: f32,
84 height: f32,
86 fill: Option<String>,
88 stroke: Option<String>,
90 },
91 Ellipse {
93 cx: f32,
95 cy: f32,
97 rx: f32,
99 ry: f32,
101 fill: Option<String>,
103 stroke: Option<String>,
105 },
106 Text {
108 x: f32,
110 y: f32,
112 content: String,
114 font_size: f32,
116 color: String,
118 },
119 Image {
121 x: f32,
123 y: f32,
125 width: f32,
127 height: f32,
129 data_url: String,
131 },
132}
133
134#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
136pub struct CanvasElement {
137 pub id: String,
139 pub element_type: ElementType,
141 pub layer_id: String,
143 pub z_index: i32,
145 pub opacity: f32,
147 pub transform: Option<Transform>,
149 pub created_by: String,
151 pub created_at: i64,
153}
154
155#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
157pub struct Layer {
158 pub id: String,
160 pub name: String,
162 pub visible: bool,
164 pub locked: bool,
166 pub opacity: f32,
168 pub z_index: i32,
170}
171
172#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
174pub struct CanvasState {
175 pub canvas_info: CanvasInfo,
177 pub elements: Vec<CanvasElement>,
179 pub layers: Vec<Layer>,
181 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#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
206pub struct RemoteCursor {
207 pub user_id: String,
209 pub user_name: String,
211 pub x: f32,
213 pub y: f32,
215 pub color: String,
217 pub last_active: i64,
219 pub tool: Option<String>,
221}
222
223#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
225pub enum HistoryActionType {
226 AddElement,
228 DeleteElement,
230 ModifyElement,
232 AddLayer,
234 DeleteLayer,
236 ModifyLayer,
238 BatchOperation,
240}
241
242impl HistoryActionType {
243 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#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
259pub struct HistoryEntry {
260 pub id: String,
262 pub action_type: HistoryActionType,
264 pub timestamp: i64,
266 pub user_id: String,
268 pub description: String,
270 pub can_undo: bool,
272 pub can_redo: bool,
274}
275
276#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
278pub enum OperationType {
279 AddElement,
281 UpdateElement,
283 DeleteElement,
285 AddLayer,
287 UpdateLayer,
289 DeleteLayer,
291}
292
293impl OperationType {
294 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#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
309pub enum OfflineStatus {
310 Pending,
312 Syncing,
314 Synced,
316 Failed(String),
318}
319
320impl OfflineStatus {
321 pub fn is_pending(&self) -> bool {
323 matches!(self, Self::Pending)
324 }
325
326 pub fn is_syncing(&self) -> bool {
328 matches!(self, Self::Syncing)
329 }
330
331 pub fn is_synced(&self) -> bool {
333 matches!(self, Self::Synced)
334 }
335
336 pub fn is_failed(&self) -> bool {
338 matches!(self, Self::Failed(_))
339 }
340}
341
342#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
344pub enum SyncState {
345 #[default]
347 Synced,
348 Syncing,
350 Pending,
352 Error,
354}
355
356#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
358pub struct OfflineOperation {
359 pub id: String,
361 pub operation_type: OperationType,
363 pub payload: String,
365 pub created_at: i64,
367 pub retry_count: u32,
369 pub status: OfflineStatus,
371}
372
373#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
375pub enum ToolType {
376 #[default]
378 Select,
379 Pen,
381 Brush,
383 Eraser,
385 Rectangle,
387 Ellipse,
389 Text,
391 Pan,
393 Zoom,
395}
396
397impl ToolType {
398 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 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 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#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
446pub struct CanvasSnapshot {
447 pub state: CanvasState,
449 pub remote_cursors: Vec<RemoteCursor>,
451 pub history: Vec<HistoryEntry>,
453 pub offline_operations: Vec<OfflineOperation>,
455 pub selected_tool: ToolType,
457 pub stroke_color: String,
459 pub fill_color: Option<String>,
461 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}