Skip to main content

oximedia_clips/note/
annotation.rs

1//! Annotation and note types.
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use uuid::Uuid;
6
7/// Unique identifier for a note.
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
9pub struct NoteId(Uuid);
10
11impl NoteId {
12    /// Creates a new random note ID.
13    #[must_use]
14    pub fn new() -> Self {
15        Self(Uuid::new_v4())
16    }
17
18    /// Creates a note ID from a UUID.
19    #[must_use]
20    pub const fn from_uuid(uuid: Uuid) -> Self {
21        Self(uuid)
22    }
23
24    /// Returns the inner UUID.
25    #[must_use]
26    pub const fn as_uuid(&self) -> &Uuid {
27        &self.0
28    }
29}
30
31impl Default for NoteId {
32    fn default() -> Self {
33        Self::new()
34    }
35}
36
37impl std::fmt::Display for NoteId {
38    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39        write!(f, "{}", self.0)
40    }
41}
42
43/// A note or annotation on a clip.
44#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct Note {
46    /// Unique identifier.
47    pub id: NoteId,
48
49    /// Note content.
50    pub content: String,
51
52    /// Optional frame position.
53    pub frame: Option<i64>,
54
55    /// Created timestamp.
56    pub created_at: DateTime<Utc>,
57
58    /// Created by user.
59    pub created_by: Option<String>,
60
61    /// Last modified timestamp.
62    pub modified_at: DateTime<Utc>,
63
64    /// Reply to another note (for threading).
65    pub reply_to: Option<NoteId>,
66}
67
68impl Note {
69    /// Creates a new note.
70    #[must_use]
71    pub fn new(content: impl Into<String>) -> Self {
72        let now = Utc::now();
73        Self {
74            id: NoteId::new(),
75            content: content.into(),
76            frame: None,
77            created_at: now,
78            created_by: None,
79            modified_at: now,
80            reply_to: None,
81        }
82    }
83
84    /// Creates a note at a specific frame.
85    #[must_use]
86    pub fn at_frame(content: impl Into<String>, frame: i64) -> Self {
87        let mut note = Self::new(content);
88        note.frame = Some(frame);
89        note
90    }
91
92    /// Creates a reply to another note.
93    #[must_use]
94    pub fn reply_to(content: impl Into<String>, reply_to: NoteId) -> Self {
95        let mut note = Self::new(content);
96        note.reply_to = Some(reply_to);
97        note
98    }
99
100    /// Sets the frame position.
101    pub fn set_frame(&mut self, frame: i64) {
102        self.frame = Some(frame);
103        self.modified_at = Utc::now();
104    }
105
106    /// Sets the creator.
107    pub fn set_created_by(&mut self, user: impl Into<String>) {
108        self.created_by = Some(user.into());
109    }
110
111    /// Updates the content.
112    pub fn set_content(&mut self, content: impl Into<String>) {
113        self.content = content.into();
114        self.modified_at = Utc::now();
115    }
116
117    /// Checks if this is a reply.
118    #[must_use]
119    pub const fn is_reply(&self) -> bool {
120        self.reply_to.is_some()
121    }
122}
123
124/// An annotation with drawing data.
125#[derive(Debug, Clone, Serialize, Deserialize)]
126pub struct Annotation {
127    /// Unique identifier.
128    pub id: NoteId,
129
130    /// Associated note.
131    pub note: Note,
132
133    /// Frame position.
134    pub frame: i64,
135
136    /// Annotation type.
137    pub annotation_type: AnnotationType,
138
139    /// Drawing data (format depends on type).
140    pub data: AnnotationData,
141}
142
143/// Type of annotation.
144#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
145pub enum AnnotationType {
146    /// Free-form drawing.
147    Drawing,
148    /// Arrow annotation.
149    Arrow,
150    /// Rectangle/box.
151    Rectangle,
152    /// Circle/ellipse.
153    Circle,
154    /// Text annotation.
155    Text,
156}
157
158/// Annotation drawing data.
159#[derive(Debug, Clone, Serialize, Deserialize)]
160pub enum AnnotationData {
161    /// Path data for drawings.
162    Path(Vec<(f64, f64)>),
163    /// Arrow with start and end points.
164    Arrow {
165        /// Start point (x, y).
166        start: (f64, f64),
167        /// End point (x, y).
168        end: (f64, f64),
169    },
170    /// Rectangle with position and size.
171    Rectangle {
172        /// X coordinate.
173        x: f64,
174        /// Y coordinate.
175        y: f64,
176        /// Width.
177        width: f64,
178        /// Height.
179        height: f64,
180    },
181    /// Circle with center and radius.
182    Circle {
183        /// Center X coordinate.
184        cx: f64,
185        /// Center Y coordinate.
186        cy: f64,
187        /// Radius.
188        radius: f64,
189    },
190    /// Text with position and content.
191    Text {
192        /// X coordinate.
193        x: f64,
194        /// Y coordinate.
195        y: f64,
196        /// Text content.
197        text: String,
198    },
199}
200
201impl Annotation {
202    /// Creates a new annotation.
203    #[must_use]
204    pub fn new(
205        note: Note,
206        frame: i64,
207        annotation_type: AnnotationType,
208        data: AnnotationData,
209    ) -> Self {
210        Self {
211            id: NoteId::new(),
212            note,
213            frame,
214            annotation_type,
215            data,
216        }
217    }
218
219    /// Creates an arrow annotation.
220    #[must_use]
221    pub fn arrow(note: Note, frame: i64, start: (f64, f64), end: (f64, f64)) -> Self {
222        Self::new(
223            note,
224            frame,
225            AnnotationType::Arrow,
226            AnnotationData::Arrow { start, end },
227        )
228    }
229
230    /// Creates a rectangle annotation.
231    #[must_use]
232    pub fn rectangle(note: Note, frame: i64, x: f64, y: f64, width: f64, height: f64) -> Self {
233        Self::new(
234            note,
235            frame,
236            AnnotationType::Rectangle,
237            AnnotationData::Rectangle {
238                x,
239                y,
240                width,
241                height,
242            },
243        )
244    }
245}
246
247#[cfg(test)]
248mod tests {
249    use super::*;
250
251    #[test]
252    fn test_note_creation() {
253        let note = Note::new("This is a test note");
254        assert_eq!(note.content, "This is a test note");
255        assert!(note.frame.is_none());
256        assert!(!note.is_reply());
257    }
258
259    #[test]
260    fn test_note_at_frame() {
261        let note = Note::at_frame("Frame note", 100);
262        assert_eq!(note.frame, Some(100));
263    }
264
265    #[test]
266    fn test_note_reply() {
267        let original = Note::new("Original note");
268        let reply = Note::reply_to("Reply", original.id);
269        assert!(reply.is_reply());
270        assert_eq!(reply.reply_to, Some(original.id));
271    }
272
273    #[test]
274    fn test_annotation() {
275        let note = Note::new("Arrow pointing here");
276        let annotation = Annotation::arrow(note, 100, (10.0, 20.0), (100.0, 200.0));
277        assert_eq!(annotation.frame, 100);
278        assert_eq!(annotation.annotation_type, AnnotationType::Arrow);
279    }
280}