Skip to main content

draw_core/
element.rs

1use serde::{Deserialize, Serialize};
2
3use crate::point::{Bounds, Point};
4use crate::style::{Arrowhead, FillStyle, FontStyle, StrokeStyle};
5
6#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
7#[serde(tag = "type")]
8pub enum Element {
9    Rectangle(ShapeElement),
10    Ellipse(ShapeElement),
11    Diamond(ShapeElement),
12    Line(LineElement),
13    Arrow(LineElement),
14    FreeDraw(FreeDrawElement),
15    Text(TextElement),
16}
17
18impl Element {
19    pub fn id(&self) -> &str {
20        match self {
21            Self::Rectangle(e) | Self::Ellipse(e) | Self::Diamond(e) => &e.id,
22            Self::Line(e) | Self::Arrow(e) => &e.id,
23            Self::FreeDraw(e) => &e.id,
24            Self::Text(e) => &e.id,
25        }
26    }
27
28    pub fn bounds(&self) -> Bounds {
29        match self {
30            Self::Rectangle(e) | Self::Ellipse(e) | Self::Diamond(e) => {
31                Bounds::new(e.x, e.y, e.width, e.height)
32            }
33            Self::Line(e) | Self::Arrow(e) => {
34                let abs_points: Vec<Point> = e
35                    .points
36                    .iter()
37                    .map(|p| Point::new(p.x + e.x, p.y + e.y))
38                    .collect();
39                Bounds::from_points(&abs_points).unwrap_or(Bounds::new(e.x, e.y, 0.0, 0.0))
40            }
41            Self::FreeDraw(e) => {
42                let abs_points: Vec<Point> = e
43                    .points
44                    .iter()
45                    .map(|p| Point::new(p.x + e.x, p.y + e.y))
46                    .collect();
47                Bounds::from_points(&abs_points).unwrap_or(Bounds::new(e.x, e.y, 0.0, 0.0))
48            }
49            Self::Text(e) => {
50                // Approximate text bounds (multiline aware)
51                let lines: Vec<&str> = e.text.split('\n').collect();
52                let max_line_len = lines.iter().map(|l| l.chars().count()).max().unwrap_or(0);
53                let width = max_line_len as f64 * e.font.size * 0.6;
54                let height = lines.len() as f64 * e.font.size * 1.2;
55                Bounds::new(e.x, e.y, width, height)
56            }
57        }
58    }
59
60    pub fn opacity(&self) -> f64 {
61        match self {
62            Self::Rectangle(e) | Self::Ellipse(e) | Self::Diamond(e) => e.opacity,
63            Self::Line(e) | Self::Arrow(e) => e.opacity,
64            Self::FreeDraw(e) => e.opacity,
65            Self::Text(e) => e.opacity,
66        }
67    }
68
69    pub fn is_locked(&self) -> bool {
70        match self {
71            Self::Rectangle(e) | Self::Ellipse(e) | Self::Diamond(e) => e.locked,
72            Self::Line(e) | Self::Arrow(e) => e.locked,
73            Self::FreeDraw(e) => e.locked,
74            Self::Text(e) => e.locked,
75        }
76    }
77
78    pub fn group_id(&self) -> Option<&str> {
79        match self {
80            Self::Rectangle(e) | Self::Ellipse(e) | Self::Diamond(e) => e.group_id.as_deref(),
81            Self::Line(e) | Self::Arrow(e) => e.group_id.as_deref(),
82            Self::FreeDraw(e) => e.group_id.as_deref(),
83            Self::Text(e) => e.group_id.as_deref(),
84        }
85    }
86}
87
88#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
89pub struct ShapeElement {
90    pub id: String,
91    pub x: f64,
92    pub y: f64,
93    pub width: f64,
94    pub height: f64,
95    #[serde(default)]
96    pub angle: f64,
97    #[serde(default)]
98    pub stroke: StrokeStyle,
99    #[serde(default)]
100    pub fill: FillStyle,
101    #[serde(default = "default_opacity")]
102    pub opacity: f64,
103    #[serde(default)]
104    pub locked: bool,
105    #[serde(default)]
106    pub group_id: Option<String>,
107}
108
109impl ShapeElement {
110    pub fn new(id: String, x: f64, y: f64, width: f64, height: f64) -> Self {
111        Self {
112            id,
113            x,
114            y,
115            width,
116            height,
117            angle: 0.0,
118            stroke: StrokeStyle::default(),
119            fill: FillStyle::default(),
120            opacity: 1.0,
121            locked: false,
122            group_id: None,
123        }
124    }
125}
126
127#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
128pub struct LineElement {
129    pub id: String,
130    pub x: f64,
131    pub y: f64,
132    pub points: Vec<Point>,
133    #[serde(default)]
134    pub stroke: StrokeStyle,
135    #[serde(default)]
136    pub start_arrowhead: Option<Arrowhead>,
137    #[serde(default)]
138    pub end_arrowhead: Option<Arrowhead>,
139    #[serde(default = "default_opacity")]
140    pub opacity: f64,
141    #[serde(default)]
142    pub locked: bool,
143    #[serde(default)]
144    pub group_id: Option<String>,
145}
146
147impl LineElement {
148    pub fn new(id: String, x: f64, y: f64, points: Vec<Point>) -> Self {
149        Self {
150            id,
151            x,
152            y,
153            points,
154            stroke: StrokeStyle::default(),
155            start_arrowhead: None,
156            end_arrowhead: None,
157            opacity: 1.0,
158            locked: false,
159            group_id: None,
160        }
161    }
162}
163
164#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
165pub struct FreeDrawElement {
166    pub id: String,
167    pub x: f64,
168    pub y: f64,
169    pub points: Vec<Point>,
170    #[serde(default)]
171    pub stroke: StrokeStyle,
172    #[serde(default = "default_opacity")]
173    pub opacity: f64,
174    #[serde(default)]
175    pub locked: bool,
176    #[serde(default)]
177    pub group_id: Option<String>,
178}
179
180impl FreeDrawElement {
181    pub fn new(id: String, x: f64, y: f64, points: Vec<Point>) -> Self {
182        Self {
183            id,
184            x,
185            y,
186            points,
187            stroke: StrokeStyle::default(),
188            opacity: 1.0,
189            locked: false,
190            group_id: None,
191        }
192    }
193}
194
195#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
196pub struct TextElement {
197    pub id: String,
198    pub x: f64,
199    pub y: f64,
200    pub text: String,
201    #[serde(default)]
202    pub font: FontStyle,
203    #[serde(default)]
204    pub stroke: StrokeStyle,
205    #[serde(default = "default_opacity")]
206    pub opacity: f64,
207    #[serde(default)]
208    pub angle: f64,
209    #[serde(default)]
210    pub locked: bool,
211    #[serde(default)]
212    pub group_id: Option<String>,
213}
214
215impl TextElement {
216    pub fn new(id: String, x: f64, y: f64, text: String) -> Self {
217        Self {
218            id,
219            x,
220            y,
221            text,
222            font: FontStyle::default(),
223            stroke: StrokeStyle::default(),
224            opacity: 1.0,
225            angle: 0.0,
226            locked: false,
227            group_id: None,
228        }
229    }
230}
231
232fn default_opacity() -> f64 {
233    1.0
234}
235
236#[cfg(test)]
237mod tests {
238    use super::*;
239
240    #[test]
241    fn test_element_id() {
242        let rect = Element::Rectangle(ShapeElement::new("r1".to_string(), 0.0, 0.0, 10.0, 10.0));
243        assert_eq!(rect.id(), "r1");
244
245        let line = Element::Line(LineElement::new(
246            "l1".to_string(),
247            0.0,
248            0.0,
249            vec![Point::new(0.0, 0.0), Point::new(10.0, 10.0)],
250        ));
251        assert_eq!(line.id(), "l1");
252
253        let text = Element::Text(TextElement::new(
254            "t1".to_string(),
255            0.0,
256            0.0,
257            "hello".to_string(),
258        ));
259        assert_eq!(text.id(), "t1");
260
261        let fd = Element::FreeDraw(FreeDrawElement::new(
262            "fd1".to_string(),
263            0.0,
264            0.0,
265            vec![Point::new(0.0, 0.0)],
266        ));
267        assert_eq!(fd.id(), "fd1");
268    }
269
270    #[test]
271    fn test_element_bounds_rectangle() {
272        let rect = Element::Rectangle(ShapeElement::new("r1".to_string(), 10.0, 20.0, 100.0, 50.0));
273        let b = rect.bounds();
274        assert_eq!(b.x, 10.0);
275        assert_eq!(b.y, 20.0);
276        assert_eq!(b.width, 100.0);
277        assert_eq!(b.height, 50.0);
278    }
279
280    #[test]
281    fn test_element_bounds_ellipse() {
282        let ellipse = Element::Ellipse(ShapeElement::new("e1".to_string(), 5.0, 10.0, 80.0, 60.0));
283        let b = ellipse.bounds();
284        assert_eq!(b.x, 5.0);
285        assert_eq!(b.y, 10.0);
286        assert_eq!(b.width, 80.0);
287        assert_eq!(b.height, 60.0);
288    }
289
290    #[test]
291    fn test_element_bounds_line() {
292        let line = Element::Line(LineElement::new(
293            "l1".to_string(),
294            10.0,
295            20.0,
296            vec![Point::new(0.0, 0.0), Point::new(50.0, 30.0)],
297        ));
298        let b = line.bounds();
299        // Absolute points: (10,20) and (60,50)
300        assert_eq!(b.x, 10.0);
301        assert_eq!(b.y, 20.0);
302        assert_eq!(b.width, 50.0);
303        assert_eq!(b.height, 30.0);
304    }
305
306    #[test]
307    fn test_element_bounds_freedraw() {
308        let fd = Element::FreeDraw(FreeDrawElement::new(
309            "fd1".to_string(),
310            5.0,
311            5.0,
312            vec![
313                Point::new(0.0, 0.0),
314                Point::new(10.0, 20.0),
315                Point::new(-5.0, 10.0),
316            ],
317        ));
318        let b = fd.bounds();
319        // Absolute points: (5,5), (15,25), (0,15)
320        assert_eq!(b.x, 0.0);
321        assert_eq!(b.y, 5.0);
322        assert_eq!(b.width, 15.0);
323        assert_eq!(b.height, 20.0);
324    }
325
326    #[test]
327    fn test_element_bounds_text_multiline() {
328        let text = Element::Text(TextElement::new(
329            "t1".to_string(),
330            0.0,
331            0.0,
332            "hello\nworld!!".to_string(),
333        ));
334        let b = text.bounds();
335        // "world!!" is 7 chars (longest line), font size default 16
336        // width = 7 * 16 * 0.6 = 67.2
337        // height = 2 * 16 * 1.2 = 38.4
338        assert!(b.width > 0.0);
339        assert!(b.height > b.width * 0.3); // 2 lines should be taller than single
340        // Verify it accounts for 2 lines
341        let single = Element::Text(TextElement::new(
342            "t2".to_string(),
343            0.0,
344            0.0,
345            "hello".to_string(),
346        ));
347        let sb = single.bounds();
348        assert!(b.height > sb.height);
349    }
350}