oak_visualize/render/
mod.rs

1#![doc = "Rendering module for converting layouts to visual formats"]
2
3use crate::{
4    geometry::{Point, Rect, Size},
5    layout::{Edge, Layout},
6};
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9
10/// Rendering configuration
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct RenderConfig {
13    pub canvas_width: f64,
14    pub canvas_height: f64,
15    pub background_color: String,
16    pub node_fill_color: String,
17    pub node_stroke_color: String,
18    pub node_stroke_width: f64,
19    pub edge_color: String,
20    pub edge_width: f64,
21    pub text_color: String,
22    pub text_size: f64,
23    pub font_family: String,
24    pub padding: f64,
25    pub show_labels: bool,
26    pub show_arrows: bool,
27    pub arrow_size: f64,
28}
29
30impl Default for RenderConfig {
31    fn default() -> Self {
32        Self {
33            canvas_width: 800.0,
34            canvas_height: 600.0,
35            background_color: "#ffffff".to_string(),
36            node_fill_color: "#e1f5fe".to_string(),
37            node_stroke_color: "#0277bd".to_string(),
38            node_stroke_width: 2.0,
39            edge_color: "#666666".to_string(),
40            edge_width: 1.5,
41            text_color: "#333333".to_string(),
42            text_size: 12.0,
43            font_family: "Arial, sans-serif".to_string(),
44            padding: 20.0,
45            show_labels: true,
46            show_arrows: true,
47            arrow_size: 8.0,
48        }
49    }
50}
51
52/// Style information for rendering elements
53#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct ElementStyle {
55    pub fill_color: Option<String>,
56    pub stroke_color: Option<String>,
57    pub stroke_width: Option<f64>,
58    pub text_color: Option<String>,
59    pub text_size: Option<f64>,
60    pub opacity: Option<f64>,
61    pub class_name: Option<String>,
62    pub attributes: HashMap<String, String>,
63}
64
65impl Default for ElementStyle {
66    fn default() -> Self {
67        Self {
68            fill_color: None,
69            stroke_color: None,
70            stroke_width: None,
71            text_color: None,
72            text_size: None,
73            opacity: None,
74            class_name: None,
75            attributes: HashMap::new(),
76        }
77    }
78}
79
80impl ElementStyle {
81    pub fn new() -> Self {
82        Self::default()
83    }
84
85    pub fn with_fill(mut self, color: String) -> Self {
86        self.fill_color = Some(color);
87        self
88    }
89
90    pub fn with_stroke(mut self, color: String, width: f64) -> Self {
91        self.stroke_color = Some(color);
92        self.stroke_width = Some(width);
93        self
94    }
95
96    pub fn with_text(mut self, color: String, size: f64) -> Self {
97        self.text_color = Some(color);
98        self.text_size = Some(size);
99        self
100    }
101
102    pub fn with_opacity(mut self, opacity: f64) -> Self {
103        self.opacity = Some(opacity);
104        self
105    }
106
107    pub fn with_class(mut self, class_name: String) -> Self {
108        self.class_name = Some(class_name);
109        self
110    }
111
112    pub fn with_attribute(mut self, key: String, value: String) -> Self {
113        self.attributes.insert(key, value);
114        self
115    }
116}
117
118/// SVG renderer for layouts
119pub struct SvgRenderer {
120    config: RenderConfig,
121    node_styles: HashMap<String, ElementStyle>,
122    edge_styles: HashMap<String, ElementStyle>,
123}
124
125impl SvgRenderer {
126    pub fn new() -> Self {
127        Self { config: RenderConfig::default(), node_styles: HashMap::new(), edge_styles: HashMap::new() }
128    }
129
130    pub fn with_config(mut self, config: RenderConfig) -> Self {
131        self.config = config;
132        self
133    }
134
135    pub fn config(&self) -> &RenderConfig {
136        &self.config
137    }
138
139    pub fn set_node_style(&mut self, node_id: String, style: ElementStyle) {
140        self.node_styles.insert(node_id, style);
141    }
142
143    pub fn set_edge_style(&mut self, edge_id: String, style: ElementStyle) {
144        self.edge_styles.insert(edge_id, style);
145    }
146
147    pub fn render_layout(&self, layout: &Layout) -> crate::Result<String> {
148        let mut svg = String::new();
149
150        // Calculate bounds and apply padding
151        let bounds = self.calculate_bounds(layout);
152        let canvas_width = bounds.size.width + 2.0 * self.config.padding;
153        let canvas_height = bounds.size.height + 2.0 * self.config.padding;
154
155        // SVG header
156        svg.push_str(&format!(
157            r#"<svg width="{}" height="{}" xmlns="http://www.w3.org/2000/svg">"#,
158            canvas_width, canvas_height
159        ));
160        svg.push('\n');
161
162        // Background
163        svg.push_str(&format!(r#"  <rect width="100%" height="100%" fill="{}"/>"#, self.config.background_color));
164        svg.push('\n');
165
166        // Define styles
167        svg.push_str("  <defs>\n");
168        svg.push_str("    <style>\n");
169        svg.push_str("      .node { cursor: pointer; }\n");
170        svg.push_str("      .node:hover { opacity: 0.8; }\n");
171        svg.push_str("      .edge { pointer-events: none; }\n");
172        svg.push_str("      .label { pointer-events: none; user-select: none; }\n");
173        svg.push_str("    </style>\n");
174
175        // Arrow marker for directed edges
176        if self.config.show_arrows {
177            svg.push_str(&format!(
178                r#"    <marker id="arrowhead" markerWidth="{}" markerHeight="{}" refX="{}" refY="{}" orient="auto">
179      <polygon points="0 0, {} {}, {} 0" fill="{}"/>
180    </marker>"#,
181                self.config.arrow_size,
182                self.config.arrow_size,
183                self.config.arrow_size,
184                self.config.arrow_size / 2.0,
185                self.config.arrow_size,
186                self.config.arrow_size,
187                self.config.arrow_size,
188                self.config.edge_color
189            ));
190            svg.push('\n');
191        }
192
193        svg.push_str("  </defs>\n");
194
195        // Transform group to apply padding offset
196        svg.push_str(&format!(
197            r#"  <g transform="translate({}, {})">"#,
198            self.config.padding - bounds.origin.x,
199            self.config.padding - bounds.origin.y
200        ));
201        svg.push('\n');
202
203        // Render edges first (so they appear behind nodes)
204        for edge in &layout.edges {
205            self.render_edge(&mut svg, edge)?;
206        }
207
208        // Render nodes
209        for (node_id, rect) in &layout.nodes {
210            self.render_node(&mut svg, node_id, rect)?;
211        }
212
213        svg.push_str("  </g>\n");
214        svg.push_str("</svg>");
215
216        Ok(svg)
217    }
218
219    fn render_node(&self, svg: &mut String, node_id: &str, rect: &Rect) -> crate::Result<()> {
220        let style = self.node_styles.get(node_id);
221
222        let fill_color = style.and_then(|s| s.fill_color.as_ref()).unwrap_or(&self.config.node_fill_color);
223        let stroke_color = style.and_then(|s| s.stroke_color.as_ref()).unwrap_or(&self.config.node_stroke_color);
224        let stroke_width = style.and_then(|s| s.stroke_width).unwrap_or(self.config.node_stroke_width);
225
226        // Node rectangle
227        svg.push_str(&format!(
228            r#"    <rect x="{}" y="{}" width="{}" height="{}" fill="{}" stroke="{}" stroke-width="{}" class="node""#,
229            rect.origin.x, rect.origin.y, rect.size.width, rect.size.height, fill_color, stroke_color, stroke_width
230        ));
231
232        // Add custom attributes
233        if let Some(style) = style {
234            if let Some(opacity) = style.opacity {
235                svg.push_str(&format!(r#" opacity="{}""#, opacity));
236            }
237            if let Some(class) = &style.class_name {
238                svg.push_str(&format!(r#" class="node {}""#, class));
239            }
240            for (key, value) in &style.attributes {
241                svg.push_str(&format!(r#" {}="{}""#, key, value));
242            }
243        }
244
245        svg.push_str("/>\n");
246
247        // Node label
248        if self.config.show_labels {
249            let text_color = style.and_then(|s| s.text_color.as_ref()).unwrap_or(&self.config.text_color);
250            let text_size = style.and_then(|s| s.text_size).unwrap_or(self.config.text_size);
251
252            let center = rect.center();
253            svg.push_str(&format!(
254                r#"    <text x="{}" y="{}" text-anchor="middle" dominant-baseline="central" fill="{}" font-size="{}" font-family="{}" class="label">{}</text>"#,
255                center.x,
256                center.y,
257                text_color,
258                text_size,
259                self.config.font_family,
260                node_id
261            ));
262            svg.push('\n');
263        }
264
265        Ok(())
266    }
267
268    fn render_edge(&self, svg: &mut String, edge: &Edge) -> crate::Result<()> {
269        let edge_id = format!("{}_{}", edge.from, edge.to);
270        let style = self.edge_styles.get(&edge_id);
271
272        let stroke_color = style.and_then(|s| s.stroke_color.as_ref()).unwrap_or(&self.config.edge_color);
273        let stroke_width = style.and_then(|s| s.stroke_width).unwrap_or(self.config.edge_width);
274
275        if edge.points.len() < 2 {
276            return Ok(());
277        }
278
279        // Create path from points
280        let mut path_data = String::new();
281        path_data.push_str(&format!("M {} {}", edge.points[0].x, edge.points[0].y));
282
283        for point in &edge.points[1..] {
284            path_data.push_str(&format!(" L {} {}", point.x, point.y));
285        }
286
287        svg.push_str(&format!(
288            r#"    <path d="{}" stroke="{}" stroke-width="{}" fill="none" class="edge""#,
289            path_data, stroke_color, stroke_width
290        ));
291
292        // Add arrow marker for directed edges
293        if self.config.show_arrows {
294            svg.push_str(r#" marker-end="url(#arrowhead)""#);
295        }
296
297        // Add custom attributes
298        if let Some(style) = style {
299            if let Some(opacity) = style.opacity {
300                svg.push_str(&format!(r#" opacity="{}""#, opacity));
301            }
302            if let Some(class) = &style.class_name {
303                svg.push_str(&format!(r#" class="edge {}""#, class));
304            }
305            for (key, value) in &style.attributes {
306                svg.push_str(&format!(r#" {}="{}""#, key, value));
307            }
308        }
309
310        svg.push_str("/>\n");
311
312        // Edge label
313        if let Some(label) = &edge.label {
314            let mid_point = if edge.points.len() >= 2 {
315                let start = &edge.points[0];
316                let end = &edge.points[edge.points.len() - 1];
317                Point::new((start.x + end.x) / 2.0, (start.y + end.y) / 2.0)
318            }
319            else {
320                edge.points[0]
321            };
322
323            let text_color = style.and_then(|s| s.text_color.as_ref()).unwrap_or(&self.config.text_color);
324            let text_size = style.and_then(|s| s.text_size).unwrap_or(self.config.text_size * 0.8);
325
326            svg.push_str(&format!(
327                r#"    <text x="{}" y="{}" text-anchor="middle" dominant-baseline="central" fill="{}" font-size="{}" font-family="{}" class="label">{}</text>"#,
328                mid_point.x,
329                mid_point.y - 5.0, // Offset slightly above the edge
330                text_color,
331                text_size,
332                self.config.font_family,
333                label
334            ));
335            svg.push('\n');
336        }
337
338        Ok(())
339    }
340
341    fn calculate_bounds(&self, layout: &Layout) -> Rect {
342        if layout.nodes.is_empty() {
343            return Rect::new(Point::origin(), Size::new(self.config.canvas_width, self.config.canvas_height));
344        }
345
346        let mut min_x = f64::INFINITY;
347        let mut min_y = f64::INFINITY;
348        let mut max_x = f64::NEG_INFINITY;
349        let mut max_y = f64::NEG_INFINITY;
350
351        for rect in layout.nodes.values() {
352            min_x = min_x.min(rect.origin.x);
353            min_y = min_y.min(rect.origin.y);
354            max_x = max_x.max(rect.origin.x + rect.size.width);
355            max_y = max_y.max(rect.origin.y + rect.size.height);
356        }
357
358        Rect::new(Point::new(min_x, min_y), Size::new(max_x - min_x, max_y - min_y))
359    }
360}
361
362impl Default for SvgRenderer {
363    fn default() -> Self {
364        Self::new()
365    }
366}
367
368/// Export formats for rendered layouts
369#[derive(Debug, Clone, Copy, PartialEq, Eq)]
370pub enum ExportFormat {
371    Svg,
372    Html,
373    Json,
374}
375
376/// Layout exporter
377pub struct LayoutExporter {
378    format: ExportFormat,
379    config: RenderConfig,
380}
381
382impl LayoutExporter {
383    pub fn new(format: ExportFormat) -> Self {
384        Self { format, config: RenderConfig::default() }
385    }
386
387    pub fn with_config(mut self, config: RenderConfig) -> Self {
388        self.config = config;
389        self
390    }
391
392    pub fn export(&self, layout: &Layout) -> crate::Result<String> {
393        match self.format {
394            ExportFormat::Svg => {
395                let renderer = SvgRenderer::new().with_config(self.config.clone());
396                renderer.render_layout(layout)
397            }
398            ExportFormat::Html => self.export_html(layout),
399            ExportFormat::Json => self.export_json(layout),
400        }
401    }
402
403    fn export_html(&self, layout: &Layout) -> crate::Result<String> {
404        let renderer = SvgRenderer::new().with_config(self.config.clone());
405        let svg_content = renderer.render_layout(layout)?;
406
407        let html = format!(
408            r#"<!DOCTYPE html>
409<html lang="en">
410<head>
411    <meta charset="UTF-8">
412    <meta name="viewport" content="width=device-width, initial-scale=1.0">
413    <title>Pex Visualization</title>
414    <style>
415        body {{
416            margin: 0;
417            padding: 20px;
418            font-family: Arial, sans-serif;
419            background-color: #f5f5f5;
420        }}
421        .container {{
422            max-width: 100%;
423            margin: 0 auto;
424            background-color: white;
425            border-radius: 8px;
426            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
427            padding: 20px;
428        }}
429        svg {{
430            max-width: 100%;
431            height: auto;
432            border: 1px solid #ddd;
433            border-radius: 4px;
434        }}
435    </style>
436</head>
437<body>
438    <div class="container">
439        <h1>Pex Visualization</h1>
440        {}
441    </div>
442</body>
443</html>"#,
444            svg_content
445        );
446
447        Ok(html)
448    }
449
450    fn export_json(&self, layout: &Layout) -> crate::Result<String> {
451        #[derive(Serialize)]
452        struct JsonLayout {
453            nodes: HashMap<String, JsonRect>,
454            edges: Vec<JsonEdge>,
455        }
456
457        #[derive(Serialize)]
458        struct JsonRect {
459            x: f64,
460            y: f64,
461            width: f64,
462            height: f64,
463        }
464
465        #[derive(Serialize)]
466        struct JsonEdge {
467            from: String,
468            to: String,
469            points: Vec<JsonPoint>,
470            label: Option<String>,
471        }
472
473        #[derive(Serialize)]
474        struct JsonPoint {
475            x: f64,
476            y: f64,
477        }
478
479        let json_layout = JsonLayout {
480            nodes: layout
481                .nodes
482                .iter()
483                .map(|(id, rect)| {
484                    (
485                        id.clone(),
486                        JsonRect { x: rect.origin.x, y: rect.origin.y, width: rect.size.width, height: rect.size.height },
487                    )
488                })
489                .collect(),
490            edges: layout
491                .edges
492                .iter()
493                .map(|edge| JsonEdge {
494                    from: edge.from.clone(),
495                    to: edge.to.clone(),
496                    points: edge.points.iter().map(|p| JsonPoint { x: p.x, y: p.y }).collect(),
497                    label: edge.label.clone(),
498                })
499                .collect(),
500        };
501
502        serde_json::to_string_pretty(&json_layout)
503            .map_err(|e| crate::Error::Serialization(format!("Failed to serialize layout to JSON: {}", e)))
504    }
505}