Skip to main content

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