Skip to main content

batuta/oracle/svg/
builder.rs

1//! SVG Builder
2//!
3//! Fluent API for constructing SVG documents.
4
5use 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/// SVG element types
14#[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    /// Render to SVG string
26    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/// SVG document builder
42#[derive(Debug)]
43pub struct SvgBuilder {
44    /// Viewport dimensions
45    viewport: Viewport,
46    /// Material palette
47    palette: MaterialPalette,
48    /// Typography scale
49    typography: MaterialTypography,
50    /// Layout engine
51    layout: LayoutEngine,
52    /// SVG elements
53    elements: Vec<SvgElement>,
54    /// Marker definitions
55    markers: Vec<ArrowMarker>,
56    /// Custom CSS styles
57    styles: Vec<String>,
58    /// SVG title
59    title: Option<String>,
60    /// SVG description
61    description: Option<String>,
62    /// Skip background rectangle (for transparent SVGs)
63    transparent: bool,
64    /// Grid protocol engine (when in grid mode)
65    grid: Option<GridProtocol>,
66}
67
68impl SvgBuilder {
69    /// Create a new SVG builder with default settings
70    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    /// Set the viewport
91    pub fn viewport(mut self, viewport: Viewport) -> Self {
92        self.viewport = viewport;
93        self.layout = LayoutEngine::new(viewport);
94        self
95    }
96
97    /// Set the viewport size
98    pub fn size(self, width: f32, height: f32) -> Self {
99        self.viewport(Viewport::new(width, height))
100    }
101
102    /// Use the document viewport (800x600)
103    pub fn document(self) -> Self {
104        self.viewport(Viewport::document())
105    }
106
107    /// Use the presentation viewport (1920x1080)
108    pub fn presentation(self) -> Self {
109        self.viewport(Viewport::presentation())
110    }
111
112    /// Set the color palette
113    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    /// Use the dark palette
120    pub fn dark_mode(self) -> Self {
121        self.palette(MaterialPalette::dark())
122    }
123
124    /// Set the title
125    pub fn title(mut self, title: &str) -> Self {
126        self.title = Some(title.to_string());
127        self
128    }
129
130    /// Set the description
131    pub fn description(mut self, desc: &str) -> Self {
132        self.description = Some(desc.to_string());
133        self
134    }
135
136    /// Skip the background rectangle (transparent SVG)
137    pub fn transparent(mut self) -> Self {
138        self.transparent = true;
139        self
140    }
141
142    /// Enable grid protocol mode with 1920x1080 viewport.
143    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    /// Allocate a named region in the grid. Only valid in grid mode.
151    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    /// Check if grid protocol mode is active.
159    pub fn is_grid_mode(&self) -> bool {
160        self.grid.is_some()
161    }
162
163    /// Inject video-mode CSS classes for `.heading`, `.body`, `.mono`.
164    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    /// Add a custom CSS style
172    pub fn add_style(mut self, css: &str) -> Self {
173        self.styles.push(css.to_string());
174        self
175    }
176
177    /// Add a rectangle
178    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    /// Add a styled rectangle
190    #[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    /// Add a circle
215    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    /// Add a styled circle
228    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    /// Add a line
251    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    /// Add a styled line
258    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    /// Add text
273    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    /// Add text with a specific style
280    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    /// Add a title (headline style)
287    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    /// Add a label (small text)
294    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    /// Add a path
301    pub fn path(mut self, path: Path) -> Self {
302        self.elements.push(SvgElement::Path(path));
303        self
304    }
305
306    /// Add an arrow marker definition
307    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    /// Add an element directly
313    pub fn element(mut self, element: SvgElement) -> Self {
314        self.elements.push(element);
315        self
316    }
317
318    /// Add a group of elements
319    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    /// Get the palette
325    pub fn get_palette(&self) -> &MaterialPalette {
326        &self.palette
327    }
328
329    /// Get the typography
330    pub fn get_typography(&self) -> &MaterialTypography {
331        &self.typography
332    }
333
334    /// Get the layout engine
335    pub fn get_layout(&self) -> &LayoutEngine {
336        &self.layout
337    }
338
339    /// Get the layout engine mutably
340    pub fn get_layout_mut(&mut self) -> &mut LayoutEngine {
341        &mut self.layout
342    }
343
344    /// Validate the SVG layout
345    pub fn validate(&self) -> Vec<String> {
346        let mut errors = Vec::new();
347
348        // Check layout errors
349        for error in self.layout.validate() {
350            errors.push(error.to_string());
351        }
352
353        // Check for valid palette colors (simplified check)
354        // In production, would validate all element colors
355
356        errors
357    }
358
359    /// Estimate the output file size
360    pub fn estimate_size(&self) -> usize {
361        // Rough estimate: base overhead + per-element overhead
362        let base = 500; // XML declaration, root element, etc.
363        let per_element = 100; // Average element size
364        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    /// Build the SVG document
371    pub fn build(self) -> String {
372        let mut svg = String::new();
373
374        // XML declaration
375        svg.push_str("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
376
377        // SVG root element
378        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        // Grid protocol manifest (before title so it's the first child comment)
386        if let Some(grid) = &self.grid {
387            svg.push_str(&format!("  {}\n", grid.manifest()));
388        }
389
390        // Title and description
391        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        // Styles
399        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        // Definitions (markers)
408        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        // Background (skipped when transparent)
417        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        // Elements
425        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
441/// Builder for component diagrams (shape-heavy)
442pub struct ComponentDiagramBuilder {
443    builder: SvgBuilder,
444    palette: SovereignPalette,
445}
446
447impl ComponentDiagramBuilder {
448    /// Create a new component diagram builder
449    pub fn new() -> Self {
450        Self { builder: SvgBuilder::new().presentation(), palette: SovereignPalette::light() }
451    }
452
453    /// Add a component box
454    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        // Component label
471        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    /// Add a connection arrow
484    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    /// Build the diagram
497    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        // Use a larger viewport with no padding issues
570        let builder = SvgBuilder::new().size(200.0, 200.0).rect("r1", 24.0, 24.0, 48.0, 48.0); // Use grid-aligned values inside content area
571
572        let errors = builder.validate();
573        // Should be valid (properly aligned and within bounds)
574        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); // Should be under 10KB
585    }
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        // Output should be under 100KB as per spec
627        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        // Light mode palette by default
835        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        // Should NOT contain background rect
875        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        // Should contain background rect
883        assert!(svg.contains("width=\"100%\" height=\"100%\""));
884    }
885
886    // ── Grid Protocol Builder Tests ────────────────────────────────────
887
888    #[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}