1use super::grid_protocol::{GridError, GridProtocol, GridSpan, PixelBounds};
6use super::layout::{LayoutEngine, Viewport, GRID_SIZE};
7use super::palette::{Color, MaterialPalette, SovereignPalette};
8#[allow(unused_imports)]
9pub use super::shapes::Size;
10use super::shapes::{ArrowMarker, Circle, Line, Path, Point, Rect, Text};
11use super::typography::{MaterialTypography, TextStyle};
12
13#[derive(Debug, Clone)]
15pub enum SvgElement {
16 Rect(Rect),
17 Circle(Circle),
18 Line(Line),
19 Path(Path),
20 Text(Text),
21 Group { id: String, elements: Vec<SvgElement> },
22}
23
24impl SvgElement {
25 pub fn to_svg(&self) -> String {
27 match self {
28 Self::Rect(r) => r.to_svg(),
29 Self::Circle(c) => c.to_svg(),
30 Self::Line(l) => l.to_svg(),
31 Self::Path(p) => p.to_svg(),
32 Self::Text(t) => t.to_svg(),
33 Self::Group { id, elements } => {
34 let children: String = elements.iter().map(|e| e.to_svg()).collect();
35 format!("<g id=\"{}\">{}</g>", id, children)
36 }
37 }
38 }
39}
40
41#[derive(Debug)]
43pub struct SvgBuilder {
44 viewport: Viewport,
46 palette: MaterialPalette,
48 typography: MaterialTypography,
50 layout: LayoutEngine,
52 elements: Vec<SvgElement>,
54 markers: Vec<ArrowMarker>,
56 styles: Vec<String>,
58 title: Option<String>,
60 description: Option<String>,
62 transparent: bool,
64 grid: Option<GridProtocol>,
66}
67
68impl SvgBuilder {
69 pub fn new() -> Self {
71 let viewport = Viewport::presentation();
72 let palette = MaterialPalette::light();
73 let typography = MaterialTypography::with_color(palette.on_surface);
74
75 Self {
76 viewport,
77 palette: palette.clone(),
78 typography,
79 layout: LayoutEngine::new(viewport),
80 elements: Vec::new(),
81 markers: Vec::new(),
82 styles: Vec::new(),
83 title: None,
84 description: None,
85 transparent: false,
86 grid: None,
87 }
88 }
89
90 pub fn viewport(mut self, viewport: Viewport) -> Self {
92 self.viewport = viewport;
93 self.layout = LayoutEngine::new(viewport);
94 self
95 }
96
97 pub fn size(self, width: f32, height: f32) -> Self {
99 self.viewport(Viewport::new(width, height))
100 }
101
102 pub fn document(self) -> Self {
104 self.viewport(Viewport::document())
105 }
106
107 pub fn presentation(self) -> Self {
109 self.viewport(Viewport::presentation())
110 }
111
112 pub fn palette(mut self, palette: MaterialPalette) -> Self {
114 self.typography = MaterialTypography::with_color(palette.on_surface);
115 self.palette = palette;
116 self
117 }
118
119 pub fn dark_mode(self) -> Self {
121 self.palette(MaterialPalette::dark())
122 }
123
124 pub fn title(mut self, title: &str) -> Self {
126 self.title = Some(title.to_string());
127 self
128 }
129
130 pub fn description(mut self, desc: &str) -> Self {
132 self.description = Some(desc.to_string());
133 self
134 }
135
136 pub fn transparent(mut self) -> Self {
138 self.transparent = true;
139 self
140 }
141
142 pub fn grid_protocol(mut self) -> Self {
144 self.viewport = Viewport::presentation();
145 self.layout = LayoutEngine::new(self.viewport);
146 self.grid = Some(GridProtocol::new());
147 self
148 }
149
150 pub fn allocate(&mut self, name: &str, span: GridSpan) -> Result<PixelBounds, GridError> {
152 match self.grid.as_mut() {
153 Some(grid) => grid.allocate(name, span),
154 None => Err(GridError::OutOfBounds { span }),
155 }
156 }
157
158 pub fn is_grid_mode(&self) -> bool {
160 self.grid.is_some()
161 }
162
163 pub fn video_styles(self) -> Self {
165 let css = r".heading { font-family: 'Segoe UI', 'Helvetica Neue', sans-serif; }
166.body { font-family: 'Segoe UI', 'Helvetica Neue', sans-serif; }
167.mono { font-family: 'Cascadia Code', 'Fira Code', 'Consolas', monospace; }";
168 self.add_style(css)
169 }
170
171 pub fn add_style(mut self, css: &str) -> Self {
173 self.styles.push(css.to_string());
174 self
175 }
176
177 pub fn rect(mut self, id: &str, x: f32, y: f32, width: f32, height: f32) -> Self {
179 let rect = Rect::new(x, y, width, height)
180 .with_fill(self.palette.surface)
181 .with_stroke(self.palette.outline, 1.0);
182
183 if self.layout.add(id, rect.clone()) {
184 self.elements.push(SvgElement::Rect(rect));
185 }
186 self
187 }
188
189 #[allow(clippy::too_many_arguments)]
191 pub fn rect_styled(
192 mut self,
193 id: &str,
194 x: f32,
195 y: f32,
196 width: f32,
197 height: f32,
198 fill: Color,
199 stroke: Option<(Color, f32)>,
200 radius: f32,
201 ) -> Self {
202 let mut rect = Rect::new(x, y, width, height).with_fill(fill).with_radius(radius);
203
204 if let Some((color, width)) = stroke {
205 rect = rect.with_stroke(color, width);
206 }
207
208 if self.layout.add(id, rect.clone()) {
209 self.elements.push(SvgElement::Rect(rect));
210 }
211 self
212 }
213
214 pub fn circle(mut self, id: &str, cx: f32, cy: f32, r: f32) -> Self {
216 let circle = Circle::new(cx, cy, r)
217 .with_fill(self.palette.primary)
218 .with_stroke(self.palette.outline, 1.0);
219
220 let bounds = circle.bounds();
221 if self.layout.add(id, bounds) {
222 self.elements.push(SvgElement::Circle(circle));
223 }
224 self
225 }
226
227 pub fn circle_styled(
229 mut self,
230 id: &str,
231 cx: f32,
232 cy: f32,
233 r: f32,
234 fill: Color,
235 stroke: Option<(Color, f32)>,
236 ) -> Self {
237 let mut circle = Circle::new(cx, cy, r).with_fill(fill);
238
239 if let Some((color, width)) = stroke {
240 circle = circle.with_stroke(color, width);
241 }
242
243 let bounds = circle.bounds();
244 if self.layout.add(id, bounds) {
245 self.elements.push(SvgElement::Circle(circle));
246 }
247 self
248 }
249
250 pub fn line(mut self, x1: f32, y1: f32, x2: f32, y2: f32) -> Self {
252 let line = Line::new(x1, y1, x2, y2).with_stroke(self.palette.outline);
253 self.elements.push(SvgElement::Line(line));
254 self
255 }
256
257 pub fn line_styled(
259 mut self,
260 x1: f32,
261 y1: f32,
262 x2: f32,
263 y2: f32,
264 color: Color,
265 width: f32,
266 ) -> Self {
267 let line = Line::new(x1, y1, x2, y2).with_stroke(color).with_stroke_width(width);
268 self.elements.push(SvgElement::Line(line));
269 self
270 }
271
272 pub fn text(mut self, x: f32, y: f32, content: &str) -> Self {
274 let text = Text::new(x, y, content).with_style(self.typography.body_medium.clone());
275 self.elements.push(SvgElement::Text(text));
276 self
277 }
278
279 pub fn text_styled(mut self, x: f32, y: f32, content: &str, style: TextStyle) -> Self {
281 let text = Text::new(x, y, content).with_style(style);
282 self.elements.push(SvgElement::Text(text));
283 self
284 }
285
286 pub fn heading(mut self, x: f32, y: f32, content: &str) -> Self {
288 let text = Text::new(x, y, content).with_style(self.typography.headline_medium.clone());
289 self.elements.push(SvgElement::Text(text));
290 self
291 }
292
293 pub fn label(mut self, x: f32, y: f32, content: &str) -> Self {
295 let text = Text::new(x, y, content).with_style(self.typography.label_medium.clone());
296 self.elements.push(SvgElement::Text(text));
297 self
298 }
299
300 pub fn path(mut self, path: Path) -> Self {
302 self.elements.push(SvgElement::Path(path));
303 self
304 }
305
306 pub fn add_arrow_marker(mut self, id: &str, color: Color) -> Self {
308 self.markers.push(ArrowMarker::new(id, color));
309 self
310 }
311
312 pub fn element(mut self, element: SvgElement) -> Self {
314 self.elements.push(element);
315 self
316 }
317
318 pub fn group(mut self, id: &str, elements: Vec<SvgElement>) -> Self {
320 self.elements.push(SvgElement::Group { id: id.to_string(), elements });
321 self
322 }
323
324 pub fn get_palette(&self) -> &MaterialPalette {
326 &self.palette
327 }
328
329 pub fn get_typography(&self) -> &MaterialTypography {
331 &self.typography
332 }
333
334 pub fn get_layout(&self) -> &LayoutEngine {
336 &self.layout
337 }
338
339 pub fn get_layout_mut(&mut self) -> &mut LayoutEngine {
341 &mut self.layout
342 }
343
344 pub fn validate(&self) -> Vec<String> {
346 let mut errors = Vec::new();
347
348 for error in self.layout.validate() {
350 errors.push(error.to_string());
351 }
352
353 errors
357 }
358
359 pub fn estimate_size(&self) -> usize {
361 let base = 500; let per_element = 100; let marker_overhead = self.markers.len() * 200;
365 let style_overhead: usize = self.styles.iter().map(|s| s.len()).sum();
366
367 base + self.elements.len() * per_element + marker_overhead + style_overhead
368 }
369
370 pub fn build(self) -> String {
372 let mut svg = String::new();
373
374 svg.push_str("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
376
377 svg.push_str(&format!(
379 "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"{}\" width=\"{}\" height=\"{}\">\n",
380 self.viewport.view_box(),
381 self.viewport.width,
382 self.viewport.height
383 ));
384
385 if let Some(grid) = &self.grid {
387 svg.push_str(&format!(" {}\n", grid.manifest()));
388 }
389
390 if let Some(title) = &self.title {
392 svg.push_str(&format!(" <title>{}</title>\n", title));
393 }
394 if let Some(desc) = &self.description {
395 svg.push_str(&format!(" <desc>{}</desc>\n", desc));
396 }
397
398 if !self.styles.is_empty() {
400 svg.push_str(" <style>\n");
401 for style in &self.styles {
402 svg.push_str(&format!(" {}\n", style));
403 }
404 svg.push_str(" </style>\n");
405 }
406
407 if !self.markers.is_empty() {
409 svg.push_str(" <defs>\n");
410 for marker in &self.markers {
411 svg.push_str(&format!(" {}\n", marker.to_svg_def()));
412 }
413 svg.push_str(" </defs>\n");
414 }
415
416 if !self.transparent {
418 svg.push_str(&format!(
419 " <rect width=\"100%\" height=\"100%\" fill=\"{}\"/>\n",
420 self.palette.background.to_css_hex()
421 ));
422 }
423
424 for element in &self.elements {
426 svg.push_str(&format!(" {}\n", element.to_svg()));
427 }
428
429 svg.push_str("</svg>\n");
430
431 svg
432 }
433}
434
435impl Default for SvgBuilder {
436 fn default() -> Self {
437 Self::new()
438 }
439}
440
441pub struct ComponentDiagramBuilder {
443 builder: SvgBuilder,
444 palette: SovereignPalette,
445}
446
447impl ComponentDiagramBuilder {
448 pub fn new() -> Self {
450 Self { builder: SvgBuilder::new().presentation(), palette: SovereignPalette::light() }
451 }
452
453 pub fn component(mut self, id: &str, x: f32, y: f32, name: &str, component_type: &str) -> Self {
455 let width = 160.0;
456 let height = 80.0;
457 let color = self.palette.component_color(component_type);
458
459 self.builder = self.builder.rect_styled(
460 id,
461 x,
462 y,
463 width,
464 height,
465 color.lighten(0.8),
466 Some((color, 2.0)),
467 GRID_SIZE,
468 );
469
470 let text_style = self
472 .builder
473 .get_typography()
474 .title_small
475 .clone()
476 .with_color(self.palette.material.on_surface);
477 self.builder =
478 self.builder.text_styled(x + width / 2.0, y + height / 2.0 + 5.0, name, text_style);
479
480 self
481 }
482
483 pub fn connect(mut self, from: Point, to: Point) -> Self {
485 self.builder = self.builder.line_styled(
486 from.x,
487 from.y,
488 to.x,
489 to.y,
490 self.palette.material.outline,
491 2.0,
492 );
493 self
494 }
495
496 pub fn build(self) -> String {
498 self.builder.build()
499 }
500}
501
502impl Default for ComponentDiagramBuilder {
503 fn default() -> Self {
504 Self::new()
505 }
506}
507
508#[cfg(test)]
509mod tests {
510 use super::*;
511
512 #[test]
513 fn test_svg_builder_creation() {
514 let builder = SvgBuilder::new();
515 assert_eq!(builder.viewport.width, 1920.0);
516 assert_eq!(builder.viewport.height, 1080.0);
517 }
518
519 #[test]
520 fn test_svg_builder_viewport() {
521 let builder = SvgBuilder::new().document();
522 assert_eq!(builder.viewport.width, 800.0);
523 assert_eq!(builder.viewport.height, 600.0);
524 }
525
526 #[test]
527 fn test_svg_builder_rect() {
528 let svg = SvgBuilder::new().size(200.0, 200.0).rect("test", 10.0, 10.0, 50.0, 50.0).build();
529
530 assert!(svg.contains("<rect"));
531 assert!(svg.contains("width=\"50\""));
532 }
533
534 #[test]
535 fn test_svg_builder_circle() {
536 let svg = SvgBuilder::new().size(200.0, 200.0).circle("test", 50.0, 50.0, 25.0).build();
537
538 assert!(svg.contains("<circle"));
539 assert!(svg.contains("r=\"25\""));
540 }
541
542 #[test]
543 fn test_svg_builder_text() {
544 let svg = SvgBuilder::new().size(200.0, 200.0).text(10.0, 20.0, "Hello").build();
545
546 assert!(svg.contains("<text"));
547 assert!(svg.contains("Hello"));
548 }
549
550 #[test]
551 fn test_svg_builder_title() {
552 let svg = SvgBuilder::new().title("Test Diagram").description("A test").build();
553
554 assert!(svg.contains("<title>Test Diagram</title>"));
555 assert!(svg.contains("<desc>A test</desc>"));
556 }
557
558 #[test]
559 fn test_svg_builder_dark_mode() {
560 let builder = SvgBuilder::new().dark_mode();
561 assert_eq!(
562 builder.palette.surface.to_css_hex(),
563 MaterialPalette::dark().surface.to_css_hex()
564 );
565 }
566
567 #[test]
568 fn test_svg_builder_validation() {
569 let builder = SvgBuilder::new().size(200.0, 200.0).rect("r1", 24.0, 24.0, 48.0, 48.0); let errors = builder.validate();
573 assert!(errors.is_empty(), "Unexpected errors: {:?}", errors);
575 }
576
577 #[test]
578 fn test_svg_builder_estimate_size() {
579 let builder =
580 SvgBuilder::new().rect("r1", 0.0, 0.0, 50.0, 50.0).rect("r2", 60.0, 0.0, 50.0, 50.0);
581
582 let size = builder.estimate_size();
583 assert!(size > 0);
584 assert!(size < 10000); }
586
587 #[test]
588 fn test_svg_element_group() {
589 let group = SvgElement::Group {
590 id: "test-group".to_string(),
591 elements: vec![
592 SvgElement::Rect(Rect::new(0.0, 0.0, 10.0, 10.0)),
593 SvgElement::Circle(Circle::new(5.0, 5.0, 2.0)),
594 ],
595 };
596
597 let svg = group.to_svg();
598 assert!(svg.contains("id=\"test-group\""));
599 assert!(svg.contains("<rect"));
600 assert!(svg.contains("<circle"));
601 }
602
603 #[test]
604 fn test_component_diagram_builder() {
605 let svg = ComponentDiagramBuilder::new()
606 .component("trueno", 100.0, 100.0, "Trueno", "trueno")
607 .component("aprender", 300.0, 100.0, "Aprender", "aprender")
608 .connect(Point::new(260.0, 140.0), Point::new(300.0, 140.0))
609 .build();
610
611 assert!(svg.contains("<svg"));
612 assert!(svg.contains("Trueno"));
613 assert!(svg.contains("Aprender"));
614 }
615
616 #[test]
617 fn test_svg_output_size() {
618 let svg = SvgBuilder::new()
619 .size(800.0, 600.0)
620 .title("Test")
621 .rect("r1", 10.0, 10.0, 100.0, 100.0)
622 .circle("c1", 200.0, 200.0, 30.0)
623 .text(50.0, 50.0, "Hello World")
624 .build();
625
626 assert!(svg.len() < 100_000, "SVG too large: {} bytes", svg.len());
628 }
629
630 #[test]
631 fn test_svg_builder_custom_viewport() {
632 let vp = Viewport::new(400.0, 300.0);
633 let builder = SvgBuilder::new().viewport(vp);
634 let svg = builder.build();
635 assert!(svg.contains("viewBox=\"0 0 400 300\""));
636 }
637
638 #[test]
639 fn test_svg_builder_document() {
640 let builder = SvgBuilder::new().document();
641 let svg = builder.build();
642 assert!(svg.contains("viewBox=\"0 0 800 600\""));
643 }
644
645 #[test]
646 fn test_svg_builder_presentation() {
647 let builder = SvgBuilder::new().presentation();
648 let svg = builder.build();
649 assert!(svg.contains("viewBox=\"0 0 1920 1080\""));
650 }
651
652 #[test]
653 fn test_svg_builder_palette() {
654 let palette = MaterialPalette::dark();
655 let builder = SvgBuilder::new().palette(palette);
656 assert_eq!(builder.get_palette().surface.to_css_hex(), "#1C1B1F");
657 }
658
659 #[test]
660 fn test_svg_builder_add_style() {
661 let svg = SvgBuilder::new().add_style(".my-class { fill: red; }").build();
662 assert!(svg.contains(".my-class { fill: red; }"));
663 }
664
665 #[test]
666 fn test_svg_builder_rect_styled() {
667 let svg = SvgBuilder::new()
668 .size(200.0, 200.0)
669 .rect_styled(
670 "styled",
671 10.0,
672 10.0,
673 50.0,
674 50.0,
675 Color::rgb(255, 0, 0),
676 Some((Color::rgb(0, 0, 0), 2.0)),
677 5.0,
678 )
679 .build();
680 assert!(svg.contains("fill=\"#FF0000\""));
681 assert!(svg.contains("stroke=\"#000000\""));
682 assert!(svg.contains("rx=\"5\""));
683 }
684
685 #[test]
686 fn test_svg_builder_circle_styled() {
687 let svg = SvgBuilder::new()
688 .size(200.0, 200.0)
689 .circle_styled(
690 "styled",
691 50.0,
692 50.0,
693 25.0,
694 Color::rgb(0, 255, 0),
695 Some((Color::rgb(0, 0, 0), 3.0)),
696 )
697 .build();
698 assert!(svg.contains("fill=\"#00FF00\""));
699 }
700
701 #[test]
702 fn test_svg_builder_line() {
703 let svg = SvgBuilder::new().size(200.0, 200.0).line(0.0, 0.0, 100.0, 100.0).build();
704 assert!(svg.contains("<line"));
705 assert!(svg.contains("x1=\"0\""));
706 assert!(svg.contains("x2=\"100\""));
707 }
708
709 #[test]
710 fn test_svg_builder_line_styled() {
711 let svg = SvgBuilder::new()
712 .size(200.0, 200.0)
713 .line_styled(0.0, 0.0, 100.0, 100.0, Color::rgb(255, 0, 0), 5.0)
714 .build();
715 assert!(svg.contains("stroke=\"#FF0000\""));
716 assert!(svg.contains("stroke-width=\"5\""));
717 }
718
719 #[test]
720 fn test_svg_builder_text_styled() {
721 use crate::oracle::svg::typography::{FontWeight, TextStyle};
722 let style = TextStyle::new(20.0, FontWeight::Bold);
723 let svg = SvgBuilder::new()
724 .size(200.0, 200.0)
725 .text_styled(10.0, 30.0, "Styled Text", style)
726 .build();
727 assert!(svg.contains("font-size=\"20\""));
728 assert!(svg.contains("font-weight=\"700\""));
729 }
730
731 #[test]
732 fn test_svg_builder_heading() {
733 let svg = SvgBuilder::new().size(200.0, 200.0).heading(10.0, 30.0, "Heading").build();
734 assert!(svg.contains("Heading"));
735 }
736
737 #[test]
738 fn test_svg_builder_label() {
739 let svg = SvgBuilder::new().size(200.0, 200.0).label(10.0, 30.0, "Label").build();
740 assert!(svg.contains("Label"));
741 }
742
743 #[test]
744 fn test_svg_builder_path() {
745 use crate::oracle::svg::shapes::Path;
746 let path = Path::new().move_to(0.0, 0.0).line_to(100.0, 100.0).close();
747 let svg = SvgBuilder::new().size(200.0, 200.0).path(path).build();
748 assert!(svg.contains("<path"));
749 assert!(svg.contains("M 0 0"));
750 }
751
752 #[test]
753 fn test_svg_builder_add_arrow_marker() {
754 let svg = SvgBuilder::new()
755 .size(200.0, 200.0)
756 .add_arrow_marker("arrow1", Color::rgb(0, 0, 255))
757 .build();
758 assert!(svg.contains("<marker"));
759 assert!(svg.contains("id=\"arrow1\""));
760 }
761
762 #[test]
763 fn test_svg_builder_element() {
764 use crate::oracle::svg::shapes::Rect;
765 let rect = Rect::new(10.0, 10.0, 50.0, 50.0);
766 let svg = SvgBuilder::new().size(200.0, 200.0).element(SvgElement::Rect(rect)).build();
767 assert!(svg.contains("<rect"));
768 }
769
770 #[test]
771 fn test_svg_builder_group() {
772 use crate::oracle::svg::shapes::{Circle, Rect};
773 let elements = vec![
774 SvgElement::Rect(Rect::new(0.0, 0.0, 10.0, 10.0)),
775 SvgElement::Circle(Circle::new(5.0, 5.0, 3.0)),
776 ];
777 let svg = SvgBuilder::new().size(200.0, 200.0).group("my-group", elements).build();
778 assert!(svg.contains("<g id=\"my-group\""));
779 }
780
781 #[test]
782 fn test_svg_builder_get_typography() {
783 let builder = SvgBuilder::new();
784 let typo = builder.get_typography();
785 assert_eq!(typo.body_medium.size, 14.0);
786 }
787
788 #[test]
789 fn test_svg_builder_get_layout() {
790 let builder = SvgBuilder::new().size(200.0, 200.0);
791 let layout = builder.get_layout();
792 assert!(layout.is_empty());
793 }
794
795 #[test]
796 fn test_svg_builder_get_layout_mut() {
797 let mut builder = SvgBuilder::new().size(200.0, 200.0);
798 let layout = builder.get_layout_mut();
799 layout.clear();
800 assert!(layout.is_empty());
801 }
802
803 #[test]
804 fn test_svg_builder_default() {
805 let builder = SvgBuilder::default();
806 let svg = builder.build();
807 assert!(svg.contains("<svg"));
808 }
809
810 #[test]
811 fn test_svg_element_to_svg_variants() {
812 use crate::oracle::svg::shapes::{Line, Path, Text};
813
814 let line = SvgElement::Line(Line::new(0.0, 0.0, 10.0, 10.0));
815 assert!(line.to_svg().contains("<line"));
816
817 let text = SvgElement::Text(Text::new(0.0, 10.0, "Test"));
818 assert!(text.to_svg().contains("<text"));
819
820 let path = SvgElement::Path(Path::new().move_to(0.0, 0.0).line_to(10.0, 10.0));
821 assert!(path.to_svg().contains("<path"));
822 }
823
824 #[test]
825 fn test_component_diagram_builder_new() {
826 let builder = ComponentDiagramBuilder::new();
827 assert!(builder.builder.elements.is_empty());
828 }
829
830 #[test]
831 fn test_svg_builder_get_palette() {
832 let builder = SvgBuilder::new();
833 let palette = builder.get_palette();
834 assert_eq!(palette.primary.to_css_hex(), MaterialPalette::light().primary.to_css_hex());
836 }
837
838 #[test]
839 fn test_component_diagram_builder_component() {
840 let builder = ComponentDiagramBuilder::new().component(
841 "c1",
842 100.0,
843 100.0,
844 "Test Component",
845 "Service",
846 );
847 assert!(!builder.builder.elements.is_empty());
848 }
849
850 #[test]
851 fn test_component_diagram_builder_connect() {
852 let builder =
853 ComponentDiagramBuilder::new().connect(Point::new(0.0, 0.0), Point::new(100.0, 100.0));
854 assert!(!builder.builder.elements.is_empty());
855 }
856
857 #[test]
858 fn test_component_diagram_builder_build() {
859 let svg =
860 ComponentDiagramBuilder::new().component("c1", 50.0, 50.0, "Service", "API").build();
861 assert!(svg.contains("<svg"));
862 assert!(svg.contains("Service"));
863 }
864
865 #[test]
866 fn test_component_diagram_builder_default() {
867 let builder = ComponentDiagramBuilder::default();
868 assert!(builder.builder.elements.is_empty());
869 }
870
871 #[test]
872 fn test_svg_builder_transparent() {
873 let svg = SvgBuilder::new().size(200.0, 200.0).transparent().build();
874 assert!(!svg.contains("width=\"100%\" height=\"100%\""));
876 assert!(svg.contains("<svg"));
877 }
878
879 #[test]
880 fn test_svg_builder_opaque_has_background() {
881 let svg = SvgBuilder::new().size(200.0, 200.0).build();
882 assert!(svg.contains("width=\"100%\" height=\"100%\""));
884 }
885
886 #[test]
889 fn test_svg_builder_grid_protocol() {
890 let mut builder = SvgBuilder::new().grid_protocol();
891 assert!(builder.is_grid_mode());
892
893 let result = builder
894 .allocate("header", crate::oracle::svg::grid_protocol::GridSpan::new(0, 0, 15, 1));
895 assert!(result.is_ok());
896 }
897
898 #[test]
899 fn test_svg_builder_grid_protocol_manifest_in_output() {
900 let mut builder = SvgBuilder::new().grid_protocol();
901 builder
902 .allocate("header", crate::oracle::svg::grid_protocol::GridSpan::new(0, 0, 15, 1))
903 .expect("unexpected failure");
904
905 let svg = builder.build();
906 assert!(svg.contains("GRID PROTOCOL MANIFEST"));
907 assert!(svg.contains("\"header\""));
908 assert!(svg.contains("viewBox=\"0 0 1920 1080\""));
909 }
910
911 #[test]
912 fn test_svg_builder_grid_protocol_overlap_rejected() {
913 let mut builder = SvgBuilder::new().grid_protocol();
914 builder
915 .allocate("a", crate::oracle::svg::grid_protocol::GridSpan::new(0, 0, 7, 4))
916 .expect("unexpected failure");
917
918 let result =
919 builder.allocate("b", crate::oracle::svg::grid_protocol::GridSpan::new(5, 3, 10, 6));
920 assert!(result.is_err());
921 }
922
923 #[test]
924 fn test_svg_builder_allocate_without_grid_mode() {
925 let mut builder = SvgBuilder::new();
926 assert!(!builder.is_grid_mode());
927
928 let result = builder
929 .allocate("header", crate::oracle::svg::grid_protocol::GridSpan::new(0, 0, 15, 1));
930 assert!(result.is_err());
931 }
932
933 #[test]
934 fn test_svg_builder_video_styles() {
935 let svg = SvgBuilder::new().video_styles().build();
936 assert!(svg.contains("Segoe UI"));
937 assert!(svg.contains("Cascadia Code"));
938 assert!(svg.contains(".heading"));
939 assert!(svg.contains(".body"));
940 assert!(svg.contains(".mono"));
941 }
942
943 #[test]
944 fn test_svg_builder_no_manifest_without_grid_mode() {
945 let svg = SvgBuilder::new().build();
946 assert!(!svg.contains("GRID PROTOCOL MANIFEST"));
947 }
948}