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