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 {
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
118pub 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 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.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 svg.push_str(&format!(r#" <rect width="100%" height="100%" fill="{}"/>"#, self.config.background_color));
164 svg.push('\n');
165
166 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 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 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 for edge in &layout.edges {
205 self.render_edge(&mut svg, edge)?;
206 }
207
208 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 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 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 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 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 if self.config.show_arrows {
294 svg.push_str(r#" marker-end="url(#arrowhead)""#);
295 }
296
297 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 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, 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
370pub enum ExportFormat {
371 Svg,
372 Html,
373 Json,
374}
375
376pub 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}