batuta/oracle/svg/renderers/
shape_heavy.rs1use 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#[derive(Debug)]
13pub struct ShapeHeavyRenderer {
14 builder: SvgBuilder,
16 palette: SovereignPalette,
18 spacing: f32,
20 box_size: Size,
22}
23
24impl ShapeHeavyRenderer {
25 pub fn new() -> Self {
27 Self {
28 builder: SvgBuilder::new().presentation(),
29 palette: SovereignPalette::light(),
30 spacing: GRID_SIZE * 4.0, box_size: Size::new(160.0, 80.0),
32 }
33 }
34
35 pub fn viewport(mut self, viewport: Viewport) -> Self {
37 self.builder = self.builder.viewport(viewport);
38 self
39 }
40
41 pub fn dark_mode(mut self) -> Self {
43 self.palette = SovereignPalette::dark();
44 self.builder = self.builder.dark_mode();
45 self
46 }
47
48 pub fn spacing(mut self, spacing: f32) -> Self {
50 self.spacing = spacing;
51 self
52 }
53
54 pub fn box_size(mut self, size: Size) -> Self {
56 self.box_size = size;
57 self
58 }
59
60 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 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 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 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 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 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 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 pub fn title(mut self, title: &str) -> Self {
182 self.builder = self.builder.title(title);
183
184 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 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 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 pub fn template(mut self, template: LayoutTemplate) -> Self {
213 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 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 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 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 #[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 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}