Skip to main content

batuta/oracle/svg/renderers/
shape_heavy.rs

1//! Shape-Heavy Renderer
2//!
3//! Optimized for architectural diagrams and component diagrams with many shapes.
4
5use crate::oracle::svg::builder::SvgBuilder;
6use crate::oracle::svg::grid_protocol::LayoutTemplate;
7use crate::oracle::svg::layout::{auto_layout, Viewport, GRID_SIZE};
8use crate::oracle::svg::palette::SovereignPalette;
9use crate::oracle::svg::shapes::{Point, Size};
10
11/// Shape-heavy renderer for architectural diagrams
12#[derive(Debug)]
13pub struct ShapeHeavyRenderer {
14    /// SVG builder
15    builder: SvgBuilder,
16    /// Palette
17    palette: SovereignPalette,
18    /// Component spacing
19    spacing: f32,
20    /// Component box dimensions
21    box_size: Size,
22}
23
24impl ShapeHeavyRenderer {
25    /// Create a new shape-heavy renderer
26    pub fn new() -> Self {
27        Self {
28            builder: SvgBuilder::new().presentation(),
29            palette: SovereignPalette::light(),
30            spacing: GRID_SIZE * 4.0, // 32px
31            box_size: Size::new(160.0, 80.0),
32        }
33    }
34
35    /// Set the viewport
36    pub fn viewport(mut self, viewport: Viewport) -> Self {
37        self.builder = self.builder.viewport(viewport);
38        self
39    }
40
41    /// Use dark mode
42    pub fn dark_mode(mut self) -> Self {
43        self.palette = SovereignPalette::dark();
44        self.builder = self.builder.dark_mode();
45        self
46    }
47
48    /// Set spacing between components
49    pub fn spacing(mut self, spacing: f32) -> Self {
50        self.spacing = spacing;
51        self
52    }
53
54    /// Set component box size
55    pub fn box_size(mut self, size: Size) -> Self {
56        self.box_size = size;
57        self
58    }
59
60    /// Add a component box
61    pub fn component(mut self, id: &str, x: f32, y: f32, name: &str, component_type: &str) -> Self {
62        let color = self.palette.component_color(component_type);
63
64        self.builder = self.builder.rect_styled(
65            id,
66            x,
67            y,
68            self.box_size.width,
69            self.box_size.height,
70            color.lighten(0.85),
71            Some((color, 2.0)),
72            GRID_SIZE,
73        );
74
75        // Component name label
76        let label_x = x + self.box_size.width / 2.0;
77        let label_y = y + self.box_size.height / 2.0 + 5.0;
78
79        let style = self
80            .builder
81            .get_typography()
82            .title_small
83            .clone()
84            .with_color(self.palette.material.on_surface)
85            .with_align(crate::oracle::svg::typography::TextAlign::Middle);
86
87        self.builder = self.builder.text_styled(label_x, label_y, name, style);
88
89        self
90    }
91
92    /// Add a layer box (containing multiple components)
93    pub fn layer(mut self, id: &str, x: f32, y: f32, width: f32, height: f32, name: &str) -> Self {
94        let color = self.palette.material.surface_variant;
95
96        self.builder = self.builder.rect_styled(
97            id,
98            x,
99            y,
100            width,
101            height,
102            color,
103            Some((self.palette.material.outline_variant, 1.0)),
104            GRID_SIZE * 2.0,
105        );
106
107        // Layer name label at top-left
108        let style = self
109            .builder
110            .get_typography()
111            .label_medium
112            .clone()
113            .with_color(self.palette.material.on_surface_variant);
114
115        self.builder =
116            self.builder.text_styled(x + GRID_SIZE * 2.0, y + GRID_SIZE * 3.0, name, style);
117
118        self
119    }
120
121    /// Add a connection line between two points
122    pub fn connect(mut self, from: Point, to: Point) -> Self {
123        self.builder = self.builder.line_styled(
124            from.x,
125            from.y,
126            to.x,
127            to.y,
128            self.palette.material.outline,
129            2.0,
130        );
131        self
132    }
133
134    /// Add a horizontal stack of components
135    pub fn horizontal_stack(mut self, components: &[(&str, &str)], start: Point) -> Self {
136        let elements: Vec<_> = components.iter().map(|(id, _)| (*id, self.box_size)).collect();
137
138        let layout = auto_layout::row(&elements, start, self.spacing);
139
140        for ((id, name), (_, rect)) in components.iter().zip(layout.iter()) {
141            let component_type = if name.to_lowercase().contains("trueno") {
142                "trueno"
143            } else if name.to_lowercase().contains("aprender") {
144                "aprender"
145            } else if name.to_lowercase().contains("realizar") {
146                "realizar"
147            } else {
148                "batuta"
149            };
150
151            self = self.component(id, rect.position.x, rect.position.y, name, component_type);
152        }
153
154        self
155    }
156
157    /// Add a vertical stack of components
158    pub fn vertical_stack(mut self, components: &[(&str, &str)], start: Point) -> Self {
159        let elements: Vec<_> = components.iter().map(|(id, _)| (*id, self.box_size)).collect();
160
161        let layout = auto_layout::column(&elements, start, self.spacing);
162
163        for ((id, name), (_, rect)) in components.iter().zip(layout.iter()) {
164            let component_type = if name.to_lowercase().contains("trueno") {
165                "trueno"
166            } else if name.to_lowercase().contains("aprender") {
167                "aprender"
168            } else if name.to_lowercase().contains("realizar") {
169                "realizar"
170            } else {
171                "batuta"
172            };
173
174            self = self.component(id, rect.position.x, rect.position.y, name, component_type);
175        }
176
177        self
178    }
179
180    /// Add a title to the diagram
181    pub fn title(mut self, title: &str) -> Self {
182        self.builder = self.builder.title(title);
183
184        // Get viewport info before modifying builder
185        let viewport = *self.builder.get_layout().viewport();
186        let style = self
187            .builder
188            .get_typography()
189            .headline_medium
190            .clone()
191            .with_color(self.palette.material.on_background);
192
193        // Also add visual title at top
194        self.builder = self.builder.text_styled(
195            viewport.padding + GRID_SIZE,
196            viewport.padding + GRID_SIZE * 4.0,
197            title,
198            style,
199        );
200
201        self
202    }
203
204    /// Enable grid protocol mode with video palette and typography.
205    pub fn grid_protocol(mut self) -> Self {
206        self.builder = self.builder.grid_protocol().video_styles();
207        self.palette = SovereignPalette::dark();
208        self
209    }
210
211    /// Apply a layout template, allocating all regions.
212    pub fn template(mut self, template: LayoutTemplate) -> Self {
213        // Ensure grid mode is active
214        if !self.builder.is_grid_mode() {
215            self = self.grid_protocol();
216        }
217
218        let allocations = template.allocations();
219        for (name, span) in allocations {
220            let _ = self.builder.allocate(name, span);
221        }
222
223        self
224    }
225
226    /// Build the SVG
227    pub fn build(self) -> String {
228        self.builder.build()
229    }
230}
231
232impl Default for ShapeHeavyRenderer {
233    fn default() -> Self {
234        Self::new()
235    }
236}
237
238#[cfg(test)]
239mod tests {
240    use super::*;
241
242    #[test]
243    fn test_shape_heavy_renderer_creation() {
244        let renderer = ShapeHeavyRenderer::new();
245        assert_eq!(renderer.spacing, 32.0);
246        assert_eq!(renderer.box_size.width, 160.0);
247    }
248
249    #[test]
250    fn test_shape_heavy_component() {
251        let svg =
252            ShapeHeavyRenderer::new().component("trueno", 100.0, 100.0, "Trueno", "trueno").build();
253
254        assert!(svg.contains("<svg"));
255        assert!(svg.contains("Trueno"));
256    }
257
258    #[test]
259    fn test_shape_heavy_layer() {
260        let svg = ShapeHeavyRenderer::new()
261            .layer("compute", 50.0, 50.0, 400.0, 200.0, "Compute Layer")
262            .build();
263
264        assert!(svg.contains("Compute Layer"));
265    }
266
267    #[test]
268    fn test_shape_heavy_horizontal_stack() {
269        let svg = ShapeHeavyRenderer::new()
270            .horizontal_stack(
271                &[("c1", "Trueno"), ("c2", "Aprender"), ("c3", "Realizar")],
272                Point::new(100.0, 100.0),
273            )
274            .build();
275
276        assert!(svg.contains("Trueno"));
277        assert!(svg.contains("Aprender"));
278        assert!(svg.contains("Realizar"));
279    }
280
281    #[test]
282    fn test_shape_heavy_dark_mode() {
283        let renderer = ShapeHeavyRenderer::new().dark_mode();
284        // Just verify it doesn't panic
285        let _svg = renderer.build();
286    }
287
288    #[test]
289    fn test_shape_heavy_with_title() {
290        let svg = ShapeHeavyRenderer::new().title("Architecture Diagram").build();
291
292        assert!(svg.contains("<title>Architecture Diagram</title>"));
293    }
294
295    #[test]
296    fn test_shape_heavy_connect() {
297        let svg = ShapeHeavyRenderer::new()
298            .connect(Point::new(100.0, 100.0), Point::new(200.0, 200.0))
299            .build();
300
301        assert!(svg.contains("<line"));
302    }
303
304    #[test]
305    fn test_shape_heavy_viewport() {
306        let viewport = Viewport::new(800.0, 600.0);
307        let renderer = ShapeHeavyRenderer::new().viewport(viewport);
308        let svg = renderer.build();
309        // Should contain the viewport dimensions
310        assert!(svg.contains("width=\"800\"") || svg.contains("width=\""));
311    }
312
313    #[test]
314    fn test_shape_heavy_spacing() {
315        let renderer = ShapeHeavyRenderer::new().spacing(64.0);
316        assert_eq!(renderer.spacing, 64.0);
317    }
318
319    #[test]
320    fn test_shape_heavy_box_size() {
321        let renderer = ShapeHeavyRenderer::new().box_size(Size::new(200.0, 100.0));
322        assert_eq!(renderer.box_size.width, 200.0);
323        assert_eq!(renderer.box_size.height, 100.0);
324    }
325
326    #[test]
327    fn test_shape_heavy_vertical_stack() {
328        let svg = ShapeHeavyRenderer::new()
329            .vertical_stack(&[("c1", "Trueno"), ("c2", "Aprender")], Point::new(100.0, 100.0))
330            .build();
331
332        assert!(svg.contains("Trueno"));
333        assert!(svg.contains("Aprender"));
334    }
335
336    #[test]
337    fn test_shape_heavy_default() {
338        let renderer = ShapeHeavyRenderer::default();
339        assert_eq!(renderer.spacing, 32.0);
340    }
341
342    #[test]
343    fn test_shape_heavy_component_different_types() {
344        let svg = ShapeHeavyRenderer::new()
345            .component("c1", 0.0, 0.0, "Batuta", "batuta")
346            .component("c2", 200.0, 0.0, "Custom", "unknown")
347            .build();
348
349        assert!(svg.contains("Batuta"));
350        assert!(svg.contains("Custom"));
351    }
352
353    // ── Grid Protocol Renderer Tests ───────────────────────────────────
354
355    #[test]
356    fn test_shape_heavy_grid_protocol() {
357        let svg = ShapeHeavyRenderer::new().grid_protocol().title("Grid Test").build();
358
359        assert!(svg.contains("viewBox=\"0 0 1920 1080\""));
360        assert!(svg.contains("GRID PROTOCOL MANIFEST"));
361    }
362
363    #[test]
364    fn test_shape_heavy_template_diagram() {
365        let svg = ShapeHeavyRenderer::new()
366            .template(LayoutTemplate::Diagram)
367            .title("Architecture")
368            .build();
369
370        assert!(svg.contains("GRID PROTOCOL MANIFEST"));
371        assert!(svg.contains("\"header\""));
372        assert!(svg.contains("\"diagram\""));
373    }
374
375    #[test]
376    fn test_shape_heavy_template_dashboard() {
377        let svg = ShapeHeavyRenderer::new().template(LayoutTemplate::Dashboard).build();
378
379        assert!(svg.contains("GRID PROTOCOL MANIFEST"));
380        assert!(svg.contains("\"top_left\""));
381        assert!(svg.contains("\"bottom_right\""));
382    }
383
384    #[test]
385    fn test_shape_heavy_template_auto_enables_grid() {
386        // template() should auto-enable grid protocol
387        let svg = ShapeHeavyRenderer::new().template(LayoutTemplate::TitleSlide).build();
388
389        assert!(svg.contains("viewBox=\"0 0 1920 1080\""));
390        assert!(svg.contains("GRID PROTOCOL MANIFEST"));
391    }
392
393    #[test]
394    fn test_shape_heavy_grid_protocol_video_styles() {
395        let svg = ShapeHeavyRenderer::new().grid_protocol().build();
396
397        assert!(svg.contains("Segoe UI"));
398        assert!(svg.contains("Cascadia Code"));
399    }
400}