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#[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#[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
109pub 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 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.push_str(&format!(r#"<svg width="{}" height="{}" xmlns="http://www.w3.org/2000/svg">"#, canvas_width, canvas_height));
148 svg.push('\n');
149
150 svg.push_str(&format!(r#" <rect width="100%" height="100%" fill="{}"/>"#, self.config.background_color));
152 svg.push('\n');
153
154 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 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 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 for edge in &layout.edges {
189 self.render_edge(&mut svg, edge)?;
190 }
191
192 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 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 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 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 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 if self.config.show_arrows {
268 svg.push_str(r#" marker-end="url(#arrowhead)""#);
269 }
270
271 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 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, 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
345pub enum ExportFormat {
346 Svg,
347 Html,
348 Json,
349}
350
351pub 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 #[derive(Serialize)]
427 struct JsonLayout {
428 nodes: HashMap<String, JsonRect>,
429 edges: Vec<JsonEdge>,
430 }
431
432 #[derive(Serialize)]
433 struct JsonRect {
434 x: f64,
435 y: f64,
436 width: f64,
437 height: f64,
438 }
439
440 #[derive(Serialize)]
441 struct JsonEdge {
442 from: String,
443 to: String,
444 points: Vec<JsonPoint>,
445 label: Option<String>,
446 }
447
448 #[derive(Serialize)]
449 struct JsonPoint {
450 x: f64,
451 y: f64,
452 }
453
454 let json_layout = JsonLayout {
455 nodes: layout
456 .nodes
457 .iter()
458 .map(|(id, node)| {
459 let rect = &node.rect;
460 (id.clone(), JsonRect { x: rect.origin.x, y: rect.origin.y, width: rect.size.width, height: rect.size.height })
461 })
462 .collect(),
463 edges: layout.edges.iter().map(|edge| JsonEdge { from: edge.from.clone(), to: edge.to.clone(), points: edge.points.iter().map(|p| JsonPoint { x: p.x, y: p.y }).collect(), label: edge.label.clone() }).collect(),
464 };
465
466 serde_json::to_string_pretty(&json_layout).map_err(|e| crate::Error::Serialization(format!("Failed to serialize layout to JSON: {}", e)))
467 }
468}