1use crate::core::escape_xml;
6
7#[derive(Clone, Debug, Copy, PartialEq, Eq)]
9pub enum ConnectorType {
10 Straight,
12 Elbow,
14 Curved,
16}
17
18impl ConnectorType {
19 pub fn preset_name(&self) -> &'static str {
21 match self {
22 ConnectorType::Straight => "straightConnector1",
23 ConnectorType::Elbow => "bentConnector3",
24 ConnectorType::Curved => "curvedConnector3",
25 }
26 }
27
28 pub fn display_name(&self) -> &'static str {
30 match self {
31 ConnectorType::Straight => "Straight Connector",
32 ConnectorType::Elbow => "Elbow Connector",
33 ConnectorType::Curved => "Curved Connector",
34 }
35 }
36}
37
38#[derive(Clone, Debug, Copy, PartialEq, Eq)]
40pub enum ArrowType {
41 None,
43 Triangle,
45 Stealth,
47 Diamond,
49 Oval,
51 Open,
53}
54
55impl ArrowType {
56 pub fn xml_value(&self) -> &'static str {
58 match self {
59 ArrowType::None => "none",
60 ArrowType::Triangle => "triangle",
61 ArrowType::Stealth => "stealth",
62 ArrowType::Diamond => "diamond",
63 ArrowType::Oval => "oval",
64 ArrowType::Open => "arrow",
65 }
66 }
67}
68
69#[derive(Clone, Debug, Copy, PartialEq, Eq)]
71pub enum ArrowSize {
72 Small,
73 Medium,
74 Large,
75}
76
77impl ArrowSize {
78 pub fn xml_value(&self) -> &'static str {
80 match self {
81 ArrowSize::Small => "sm",
82 ArrowSize::Medium => "med",
83 ArrowSize::Large => "lg",
84 }
85 }
86}
87
88#[derive(Clone, Debug, Copy, PartialEq, Eq)]
90pub enum ConnectionSite {
91 Top,
93 Bottom,
95 Left,
97 Right,
99 TopLeft,
101 TopRight,
103 BottomLeft,
105 BottomRight,
107 Center,
109}
110
111impl ConnectionSite {
112 pub fn index(&self) -> u32 {
114 match self {
115 ConnectionSite::Top => 0,
116 ConnectionSite::Right => 1,
117 ConnectionSite::Bottom => 2,
118 ConnectionSite::Left => 3,
119 ConnectionSite::TopLeft => 4,
120 ConnectionSite::TopRight => 5,
121 ConnectionSite::BottomRight => 6,
122 ConnectionSite::BottomLeft => 7,
123 ConnectionSite::Center => 8,
124 }
125 }
126}
127
128#[derive(Clone, Debug)]
130pub struct ConnectorLine {
131 pub color: String,
133 pub width: u32,
135 pub dash: LineDash,
137}
138
139impl Default for ConnectorLine {
140 fn default() -> Self {
141 ConnectorLine {
142 color: "000000".to_string(),
143 width: 12700, dash: LineDash::Solid,
145 }
146 }
147}
148
149impl ConnectorLine {
150 pub fn new(color: &str, width: u32) -> Self {
152 ConnectorLine {
153 color: color.trim_start_matches('#').to_uppercase(),
154 width,
155 dash: LineDash::Solid,
156 }
157 }
158
159 pub fn with_dash(mut self, dash: LineDash) -> Self {
161 self.dash = dash;
162 self
163 }
164}
165
166#[derive(Clone, Debug, Copy, PartialEq, Eq)]
168pub enum LineDash {
169 Solid,
170 Dash,
171 Dot,
172 DashDot,
173 DashDotDot,
174 LongDash,
175 LongDashDot,
176}
177
178impl LineDash {
179 pub fn xml_value(&self) -> &'static str {
181 match self {
182 LineDash::Solid => "solid",
183 LineDash::Dash => "dash",
184 LineDash::Dot => "dot",
185 LineDash::DashDot => "dashDot",
186 LineDash::DashDotDot => "lgDashDotDot",
187 LineDash::LongDash => "lgDash",
188 LineDash::LongDashDot => "lgDashDot",
189 }
190 }
191}
192
193#[derive(Clone, Debug)]
195pub struct Connector {
196 pub connector_type: ConnectorType,
198 pub start_x: u32,
200 pub start_y: u32,
202 pub end_x: u32,
204 pub end_y: u32,
206 pub line: ConnectorLine,
208 pub start_arrow: ArrowType,
210 pub end_arrow: ArrowType,
212 pub arrow_size: ArrowSize,
214 pub start_shape_id: Option<u32>,
216 pub start_site: Option<ConnectionSite>,
218 pub end_shape_id: Option<u32>,
220 pub end_site: Option<ConnectionSite>,
222 pub label: Option<String>,
224}
225
226impl Connector {
227 pub fn new(
229 connector_type: ConnectorType,
230 start_x: u32,
231 start_y: u32,
232 end_x: u32,
233 end_y: u32,
234 ) -> Self {
235 Connector {
236 connector_type,
237 start_x,
238 start_y,
239 end_x,
240 end_y,
241 line: ConnectorLine::default(),
242 start_arrow: ArrowType::None,
243 end_arrow: ArrowType::None,
244 arrow_size: ArrowSize::Medium,
245 start_shape_id: None,
246 start_site: None,
247 end_shape_id: None,
248 end_site: None,
249 label: None,
250 }
251 }
252
253 pub fn straight(start_x: u32, start_y: u32, end_x: u32, end_y: u32) -> Self {
255 Self::new(ConnectorType::Straight, start_x, start_y, end_x, end_y)
256 }
257
258 pub fn elbow(start_x: u32, start_y: u32, end_x: u32, end_y: u32) -> Self {
260 Self::new(ConnectorType::Elbow, start_x, start_y, end_x, end_y)
261 }
262
263 pub fn curved(start_x: u32, start_y: u32, end_x: u32, end_y: u32) -> Self {
265 Self::new(ConnectorType::Curved, start_x, start_y, end_x, end_y)
266 }
267
268 pub fn with_line(mut self, line: ConnectorLine) -> Self {
270 self.line = line;
271 self
272 }
273
274 pub fn with_color(mut self, color: &str) -> Self {
276 self.line.color = color.trim_start_matches('#').to_uppercase();
277 self
278 }
279
280 pub fn with_width(mut self, width: u32) -> Self {
282 self.line.width = width;
283 self
284 }
285
286 pub fn with_start_arrow(mut self, arrow: ArrowType) -> Self {
288 self.start_arrow = arrow;
289 self
290 }
291
292 pub fn with_end_arrow(mut self, arrow: ArrowType) -> Self {
294 self.end_arrow = arrow;
295 self
296 }
297
298 pub fn with_arrows(mut self, start: ArrowType, end: ArrowType) -> Self {
300 self.start_arrow = start;
301 self.end_arrow = end;
302 self
303 }
304
305 pub fn with_arrow_size(mut self, size: ArrowSize) -> Self {
307 self.arrow_size = size;
308 self
309 }
310
311 pub fn connect_start(mut self, shape_id: u32, site: ConnectionSite) -> Self {
313 self.start_shape_id = Some(shape_id);
314 self.start_site = Some(site);
315 self
316 }
317
318 pub fn connect_end(mut self, shape_id: u32, site: ConnectionSite) -> Self {
320 self.end_shape_id = Some(shape_id);
321 self.end_site = Some(site);
322 self
323 }
324
325 pub fn with_label(mut self, label: &str) -> Self {
327 self.label = Some(label.to_string());
328 self
329 }
330
331 fn width(&self) -> u32 {
333 self.end_x.abs_diff(self.start_x)
334 }
335
336 fn height(&self) -> u32 {
338 self.end_y.abs_diff(self.start_y)
339 }
340
341 fn flip_h(&self) -> bool {
343 self.end_x < self.start_x
344 }
345
346 fn flip_v(&self) -> bool {
348 self.end_y < self.start_y
349 }
350}
351
352pub fn generate_connector_xml(connector: &Connector, shape_id: usize) -> String {
354 let x = connector.start_x.min(connector.end_x);
355 let y = connector.start_y.min(connector.end_y);
356 let cx = connector.width();
357 let cy = connector.height();
358
359 let flip_h = if connector.flip_h() { " flipH=\"1\"" } else { "" };
360 let flip_v = if connector.flip_v() { " flipV=\"1\"" } else { "" };
361
362 let mut xml = format!(
363 r#"<p:cxnSp>
364<p:nvCxnSpPr>
365<p:cNvPr id="{}" name="Connector {}"/>
366<p:cNvCxnSpPr>"#,
367 shape_id, shape_id
368 );
369
370 if let (Some(start_id), Some(start_site)) = (connector.start_shape_id, connector.start_site) {
372 xml.push_str(&format!(
373 r#"
374<a:stCxn id="{}" idx="{}"/>"#,
375 start_id, start_site.index()
376 ));
377 }
378
379 if let (Some(end_id), Some(end_site)) = (connector.end_shape_id, connector.end_site) {
380 xml.push_str(&format!(
381 r#"
382<a:endCxn id="{}" idx="{}"/>"#,
383 end_id, end_site.index()
384 ));
385 }
386
387 xml.push_str(&format!(
388 r#"
389</p:cNvCxnSpPr>
390<p:nvPr/>
391</p:nvCxnSpPr>
392<p:spPr>
393<a:xfrm{}{}>
394<a:off x="{}" y="{}"/>
395<a:ext cx="{}" cy="{}"/>
396</a:xfrm>
397<a:prstGeom prst="{}">
398<a:avLst/>
399</a:prstGeom>
400<a:ln w="{}">
401<a:solidFill>
402<a:srgbClr val="{}"/>
403</a:solidFill>
404<a:prstDash val="{}"/>"#,
405 flip_h, flip_v,
406 x, y, cx, cy,
407 connector.connector_type.preset_name(),
408 connector.line.width,
409 connector.line.color,
410 connector.line.dash.xml_value()
411 ));
412
413 if connector.start_arrow != ArrowType::None {
415 xml.push_str(&format!(
416 r#"
417<a:headEnd type="{}" w="{}" len="{}"/>"#,
418 connector.start_arrow.xml_value(),
419 connector.arrow_size.xml_value(),
420 connector.arrow_size.xml_value()
421 ));
422 }
423
424 if connector.end_arrow != ArrowType::None {
425 xml.push_str(&format!(
426 r#"
427<a:tailEnd type="{}" w="{}" len="{}"/>"#,
428 connector.end_arrow.xml_value(),
429 connector.arrow_size.xml_value(),
430 connector.arrow_size.xml_value()
431 ));
432 }
433
434 xml.push_str(r#"
435</a:ln>
436</p:spPr>"#);
437
438 if let Some(label) = &connector.label {
440 xml.push_str(&format!(
441 r#"
442<p:txBody>
443<a:bodyPr/>
444<a:lstStyle/>
445<a:p>
446<a:r>
447<a:rPr lang="en-US" sz="1000"/>
448<a:t>{}</a:t>
449</a:r>
450</a:p>
451</p:txBody>"#,
452 escape_xml(label)
453 ));
454 }
455
456 xml.push_str(r#"
457</p:cxnSp>"#);
458
459 xml
460}
461
462#[cfg(test)]
463mod tests {
464 use super::*;
465
466 #[test]
467 fn test_connector_type_preset() {
468 assert_eq!(ConnectorType::Straight.preset_name(), "straightConnector1");
469 assert_eq!(ConnectorType::Elbow.preset_name(), "bentConnector3");
470 assert_eq!(ConnectorType::Curved.preset_name(), "curvedConnector3");
471 }
472
473 #[test]
474 fn test_arrow_type_xml() {
475 assert_eq!(ArrowType::None.xml_value(), "none");
476 assert_eq!(ArrowType::Triangle.xml_value(), "triangle");
477 assert_eq!(ArrowType::Stealth.xml_value(), "stealth");
478 }
479
480 #[test]
481 fn test_connector_builder() {
482 let conn = Connector::straight(0, 0, 1000000, 500000)
483 .with_color("FF0000")
484 .with_end_arrow(ArrowType::Triangle);
485
486 assert_eq!(conn.line.color, "FF0000");
487 assert_eq!(conn.end_arrow, ArrowType::Triangle);
488 }
489
490 #[test]
491 fn test_connector_with_connections() {
492 let conn = Connector::elbow(0, 0, 1000000, 500000)
493 .connect_start(1, ConnectionSite::Right)
494 .connect_end(2, ConnectionSite::Left);
495
496 assert_eq!(conn.start_shape_id, Some(1));
497 assert_eq!(conn.start_site, Some(ConnectionSite::Right));
498 assert_eq!(conn.end_shape_id, Some(2));
499 assert_eq!(conn.end_site, Some(ConnectionSite::Left));
500 }
501
502 #[test]
503 fn test_generate_connector_xml() {
504 let conn = Connector::straight(0, 0, 1000000, 500000)
505 .with_end_arrow(ArrowType::Triangle);
506
507 let xml = generate_connector_xml(&conn, 1);
508 assert!(xml.contains("p:cxnSp"));
509 assert!(xml.contains("straightConnector1"));
510 assert!(xml.contains("tailEnd"));
511 }
512
513 #[test]
514 fn test_connector_with_label() {
515 let conn = Connector::straight(0, 0, 1000000, 500000)
516 .with_label("Connection");
517
518 let xml = generate_connector_xml(&conn, 1);
519 assert!(xml.contains("Connection"));
520 assert!(xml.contains("p:txBody"));
521 }
522
523 #[test]
524 fn test_line_dash_styles() {
525 assert_eq!(LineDash::Solid.xml_value(), "solid");
526 assert_eq!(LineDash::Dash.xml_value(), "dash");
527 assert_eq!(LineDash::Dot.xml_value(), "dot");
528 assert_eq!(LineDash::DashDot.xml_value(), "dashDot");
529 }
530
531 #[test]
532 fn test_connection_site_index() {
533 assert_eq!(ConnectionSite::Top.index(), 0);
534 assert_eq!(ConnectionSite::Right.index(), 1);
535 assert_eq!(ConnectionSite::Bottom.index(), 2);
536 assert_eq!(ConnectionSite::Left.index(), 3);
537 }
538}