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 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 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 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 assert!(b.width > 0.0);
339 assert!(b.height > b.width * 0.3); 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}