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