use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CanvasInfo {
pub id: String,
pub entity_id: String,
pub name: String,
pub width: u32,
pub height: u32,
pub created_at: i64,
pub updated_at: i64,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Point {
pub x: f32,
pub y: f32,
pub pressure: Option<f32>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Transform {
pub translate_x: f32,
pub translate_y: f32,
pub scale_x: f32,
pub scale_y: f32,
pub rotation: f32,
}
impl Default for Transform {
fn default() -> Self {
Self {
translate_x: 0.0,
translate_y: 0.0,
scale_x: 1.0,
scale_y: 1.0,
rotation: 0.0,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum ElementType {
Path {
points: Vec<Point>,
stroke_width: f32,
color: String,
},
Rectangle {
x: f32,
y: f32,
width: f32,
height: f32,
fill: Option<String>,
stroke: Option<String>,
},
Ellipse {
cx: f32,
cy: f32,
rx: f32,
ry: f32,
fill: Option<String>,
stroke: Option<String>,
},
Text {
x: f32,
y: f32,
content: String,
font_size: f32,
color: String,
},
Image {
x: f32,
y: f32,
width: f32,
height: f32,
data_url: String,
},
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CanvasElement {
pub id: String,
pub element_type: ElementType,
pub layer_id: String,
pub z_index: i32,
pub opacity: f32,
pub transform: Option<Transform>,
pub created_by: String,
pub created_at: i64,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Layer {
pub id: String,
pub name: String,
pub visible: bool,
pub locked: bool,
pub opacity: f32,
pub z_index: i32,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CanvasState {
pub canvas_info: CanvasInfo,
pub elements: Vec<CanvasElement>,
pub layers: Vec<Layer>,
pub active_layer_id: String,
}
impl Default for CanvasState {
fn default() -> Self {
Self {
canvas_info: CanvasInfo {
id: String::new(),
entity_id: String::new(),
name: String::new(),
width: 1920,
height: 1080,
created_at: 0,
updated_at: 0,
},
elements: Vec::new(),
layers: Vec::new(),
active_layer_id: String::new(),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct RemoteCursor {
pub user_id: String,
pub user_name: String,
pub x: f32,
pub y: f32,
pub color: String,
pub last_active: i64,
pub tool: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum HistoryActionType {
AddElement,
DeleteElement,
ModifyElement,
AddLayer,
DeleteLayer,
ModifyLayer,
BatchOperation,
}
impl HistoryActionType {
pub fn label(&self) -> &'static str {
match self {
Self::AddElement => "Add Element",
Self::DeleteElement => "Delete Element",
Self::ModifyElement => "Modify Element",
Self::AddLayer => "Add Layer",
Self::DeleteLayer => "Delete Layer",
Self::ModifyLayer => "Modify Layer",
Self::BatchOperation => "Batch Operation",
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct HistoryEntry {
pub id: String,
pub action_type: HistoryActionType,
pub timestamp: i64,
pub user_id: String,
pub description: String,
pub can_undo: bool,
pub can_redo: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum OperationType {
AddElement,
UpdateElement,
DeleteElement,
AddLayer,
UpdateLayer,
DeleteLayer,
}
impl OperationType {
pub fn label(&self) -> &'static str {
match self {
Self::AddElement => "Add Element",
Self::UpdateElement => "Update Element",
Self::DeleteElement => "Delete Element",
Self::AddLayer => "Add Layer",
Self::UpdateLayer => "Update Layer",
Self::DeleteLayer => "Delete Layer",
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum OfflineStatus {
Pending,
Syncing,
Synced,
Failed(String),
}
impl OfflineStatus {
pub fn is_pending(&self) -> bool {
matches!(self, Self::Pending)
}
pub fn is_syncing(&self) -> bool {
matches!(self, Self::Syncing)
}
pub fn is_synced(&self) -> bool {
matches!(self, Self::Synced)
}
pub fn is_failed(&self) -> bool {
matches!(self, Self::Failed(_))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
pub enum SyncState {
#[default]
Synced,
Syncing,
Pending,
Error,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct OfflineOperation {
pub id: String,
pub operation_type: OperationType,
pub payload: String,
pub created_at: i64,
pub retry_count: u32,
pub status: OfflineStatus,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum ToolType {
#[default]
Select,
Pen,
Brush,
Eraser,
Rectangle,
Ellipse,
Text,
Pan,
Zoom,
}
impl ToolType {
pub fn label(&self) -> &'static str {
match self {
Self::Select => "Select",
Self::Pen => "Pen",
Self::Brush => "Brush",
Self::Eraser => "Eraser",
Self::Rectangle => "Rectangle",
Self::Ellipse => "Ellipse",
Self::Text => "Text",
Self::Pan => "Pan",
Self::Zoom => "Zoom",
}
}
pub fn icon(&self) -> &'static str {
match self {
Self::Select => "pointer",
Self::Pen => "pen",
Self::Brush => "brush",
Self::Eraser => "eraser",
Self::Rectangle => "square",
Self::Ellipse => "circle",
Self::Text => "type",
Self::Pan => "move",
Self::Zoom => "zoom-in",
}
}
pub fn all() -> &'static [ToolType] {
&[
Self::Select,
Self::Pen,
Self::Brush,
Self::Eraser,
Self::Rectangle,
Self::Ellipse,
Self::Text,
Self::Pan,
Self::Zoom,
]
}
}
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
pub struct CanvasSnapshot {
pub state: CanvasState,
pub remote_cursors: Vec<RemoteCursor>,
pub history: Vec<HistoryEntry>,
pub offline_operations: Vec<OfflineOperation>,
pub selected_tool: ToolType,
pub stroke_color: String,
pub fill_color: Option<String>,
pub stroke_width: f32,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn transform_default_identity() {
let t = Transform::default();
assert_eq!(t.translate_x, 0.0);
assert_eq!(t.translate_y, 0.0);
assert_eq!(t.scale_x, 1.0);
assert_eq!(t.scale_y, 1.0);
assert_eq!(t.rotation, 0.0);
}
#[test]
fn tool_type_labels() {
assert_eq!(ToolType::Select.label(), "Select");
assert_eq!(ToolType::Pen.label(), "Pen");
assert_eq!(ToolType::Brush.label(), "Brush");
assert_eq!(ToolType::Eraser.label(), "Eraser");
assert_eq!(ToolType::Rectangle.label(), "Rectangle");
assert_eq!(ToolType::Ellipse.label(), "Ellipse");
assert_eq!(ToolType::Text.label(), "Text");
assert_eq!(ToolType::Pan.label(), "Pan");
assert_eq!(ToolType::Zoom.label(), "Zoom");
}
#[test]
fn tool_type_icons() {
assert_eq!(ToolType::Select.icon(), "pointer");
assert_eq!(ToolType::Pen.icon(), "pen");
assert_eq!(ToolType::Eraser.icon(), "eraser");
}
#[test]
fn tool_type_all_variants() {
let all = ToolType::all();
assert_eq!(all.len(), 9);
assert!(all.contains(&ToolType::Select));
assert!(all.contains(&ToolType::Zoom));
}
#[test]
fn history_action_type_labels() {
assert_eq!(HistoryActionType::AddElement.label(), "Add Element");
assert_eq!(HistoryActionType::DeleteElement.label(), "Delete Element");
assert_eq!(HistoryActionType::ModifyElement.label(), "Modify Element");
assert_eq!(HistoryActionType::AddLayer.label(), "Add Layer");
assert_eq!(HistoryActionType::DeleteLayer.label(), "Delete Layer");
assert_eq!(HistoryActionType::ModifyLayer.label(), "Modify Layer");
assert_eq!(HistoryActionType::BatchOperation.label(), "Batch Operation");
}
#[test]
fn operation_type_labels() {
assert_eq!(OperationType::AddElement.label(), "Add Element");
assert_eq!(OperationType::UpdateElement.label(), "Update Element");
assert_eq!(OperationType::DeleteElement.label(), "Delete Element");
assert_eq!(OperationType::AddLayer.label(), "Add Layer");
assert_eq!(OperationType::UpdateLayer.label(), "Update Layer");
assert_eq!(OperationType::DeleteLayer.label(), "Delete Layer");
}
#[test]
fn offline_status_predicates() {
assert!(OfflineStatus::Pending.is_pending());
assert!(!OfflineStatus::Pending.is_syncing());
assert!(OfflineStatus::Syncing.is_syncing());
assert!(!OfflineStatus::Syncing.is_pending());
assert!(OfflineStatus::Synced.is_synced());
assert!(!OfflineStatus::Synced.is_failed());
let failed = OfflineStatus::Failed("error".to_string());
assert!(failed.is_failed());
assert!(!failed.is_synced());
}
#[test]
fn canvas_state_default() {
let state = CanvasState::default();
assert!(state.elements.is_empty());
assert!(state.layers.is_empty());
assert!(state.active_layer_id.is_empty());
assert_eq!(state.canvas_info.width, 1920);
assert_eq!(state.canvas_info.height, 1080);
}
#[test]
fn point_serialization() {
let point = Point {
x: 10.5,
y: 20.3,
pressure: Some(0.8),
};
let json = serde_json::to_string(&point).unwrap();
let deserialized: Point = serde_json::from_str(&json).unwrap();
assert_eq!(point, deserialized);
}
#[test]
fn element_type_path() {
let path = ElementType::Path {
points: vec![
Point {
x: 0.0,
y: 0.0,
pressure: None,
},
Point {
x: 100.0,
y: 100.0,
pressure: Some(0.5),
},
],
stroke_width: 2.0,
color: "#000000".to_string(),
};
if let ElementType::Path {
points,
stroke_width,
..
} = path
{
assert_eq!(points.len(), 2);
assert_eq!(stroke_width, 2.0);
} else {
panic!("Expected Path variant");
}
}
#[test]
fn layer_creation() {
let layer = Layer {
id: "layer-1".to_string(),
name: "Background".to_string(),
visible: true,
locked: false,
opacity: 1.0,
z_index: 0,
};
assert!(layer.visible);
assert!(!layer.locked);
assert_eq!(layer.opacity, 1.0);
}
}