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#[derive(Debug, Clone)]
11#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
12pub struct RenderConfig {
13 pub canvas_width: f64,
15 pub canvas_height: f64,
17 pub background_color: String,
19 pub node_fill_color: String,
21 pub node_stroke_color: String,
23 pub node_stroke_width: f64,
25 pub edge_color: String,
27 pub edge_width: f64,
29 pub text_color: String,
31 pub text_size: f64,
33 pub font_family: String,
35 pub padding: f64,
37 pub show_labels: bool,
39 pub show_arrows: bool,
41 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#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
69pub struct ElementStyle {
70 pub fill_color: Option<String>,
72 pub stroke_color: Option<String>,
74 pub stroke_width: Option<f64>,
76 pub text_color: Option<String>,
78 pub text_size: Option<f64>,
80 pub opacity: Option<f64>,
82 pub class_name: Option<String>,
84 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 pub fn new() -> Self {
97 Self::default()
98 }
99
100 pub fn with_fill(mut self, color: String) -> Self {
102 self.fill_color = Some(color);
103 self
104 }
105
106 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 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 pub fn with_opacity(mut self, opacity: f64) -> Self {
122 self.opacity = Some(opacity);
123 self
124 }
125
126 pub fn with_class(mut self, class_name: String) -> Self {
128 self.class_name = Some(class_name);
129 self
130 }
131
132 pub fn with_attribute(mut self, key: String, value: String) -> Self {
134 self.attributes.insert(key, value);
135 self
136 }
137}
138
139pub struct SvgRenderer {
141 config: RenderConfig,
142 node_styles: HashMap<String, ElementStyle>,
143 edge_styles: HashMap<String, ElementStyle>,
144}
145
146impl SvgRenderer {
147 pub fn new() -> Self {
149 Self { config: RenderConfig::default(), node_styles: HashMap::new(), edge_styles: HashMap::new() }
150 }
151
152 pub fn with_config(mut self, config: RenderConfig) -> Self {
154 self.config = config;
155 self
156 }
157
158 pub fn config(&self) -> &RenderConfig {
160 &self.config
161 }
162
163 pub fn set_node_style(&mut self, node_id: String, style: ElementStyle) {
165 self.node_styles.insert(node_id, style);
166 }
167
168 pub fn set_edge_style(&mut self, edge_id: String, style: ElementStyle) {
170 self.edge_styles.insert(edge_id, style);
171 }
172
173 pub fn render_layout(&self, layout: &Layout) -> crate::Result<String> {
175 let mut svg = String::new();
176
177 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.push_str(&format!(r#"<svg width="{}" height="{}" xmlns="http://www.w3.org/2000/svg">"#, canvas_width, canvas_height));
184 svg.push('\n');
185
186 svg.push_str(&format!(r#" <rect width="100%" height="100%" fill="{}"/>"#, self.config.background_color));
188 svg.push('\n');
189
190 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 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 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 for edge in &layout.edges {
225 self.render_edge(&mut svg, edge)?
226 }
227
228 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 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 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 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 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 if self.config.show_arrows {
304 svg.push_str(r#" marker-end="url(#arrowhead)""#)
305 }
306
307 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 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, 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
381pub enum ExportFormat {
382 Svg,
384 Html,
386 Json,
388}
389
390pub struct LayoutExporter {
392 format: ExportFormat,
393 config: RenderConfig,
394}
395
396impl LayoutExporter {
397 pub fn new(format: ExportFormat) -> Self {
399 Self { format, config: RenderConfig::default() }
400 }
401
402 pub fn with_config(mut self, config: RenderConfig) -> Self {
404 self.config = config;
405 self
406 }
407
408 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 => {
417 #[cfg(feature = "serde")]
418 {
419 self.export_json(layout)
420 }
421 #[cfg(not(feature = "serde"))]
422 {
423 Err(crate::Error::msg("JSON export requires 'serde' feature"))
424 }
425 }
426 }
427 }
428
429 fn export_html(&self, layout: &Layout) -> crate::Result<String> {
430 let renderer = SvgRenderer::new().with_config(self.config.clone());
431 let svg_content = renderer.render_layout(layout)?;
432
433 let html = format!(
434 r#"<!DOCTYPE html>
435<html lang="en">
436<head>
437 <meta charset="UTF-8">
438 <meta name="viewport" content="width=device-width, initial-scale=1.0">
439 <title>Pex Visualization</title>
440 <style>
441 body {{
442 margin: 0;
443 padding: 20px;
444 font-family: Arial, sans-serif;
445 background-color: #f5f5f5
446 }}
447 .container {{
448 max-width: 100%;
449 margin: 0 auto;
450 background-color: white;
451 border-radius: 8px;
452 box-shadow: 0 2px 10px rgba(0,0,0,0.1);
453 padding: 20px
454 }}
455 svg {{
456 max-width: 100%;
457 height: auto;
458 border: 1px solid #ddd;
459 border-radius: 4px
460 }}
461 </style>
462</head>
463<body>
464 <div class="container">
465 <h1>Pex Visualization</h1>
466 {}
467 </div>
468</body>
469</html>"#,
470 svg_content
471 );
472
473 Ok(html)
474 }
475
476 #[cfg(feature = "serde")]
477 fn export_json(&self, layout: &Layout) -> crate::Result<String> {
478 let mut nodes = std::collections::HashMap::new();
479 for (id, node) in &layout.nodes {
480 let rect = &node.rect;
481 nodes.insert(
482 id.clone(),
483 serde_json::json!({
484 "x": rect.origin.x,
485 "y": rect.origin.y,
486 "width": rect.size.width,
487 "height": rect.size.height
488 }),
489 );
490 }
491
492 let mut edges = Vec::new();
493 for edge in &layout.edges {
494 let mut points = Vec::new();
495 for p in &edge.points {
496 points.push(serde_json::json!({
497 "x": p.x,
498 "y": p.y
499 }))
500 }
501 edges.push(serde_json::json!({
502 "from": edge.from.clone(),
503 "to": edge.to.clone(),
504 "points": points,
505 "label": edge.label.clone()
506 }))
507 }
508
509 let json_layout = serde_json::json!({
510 "nodes": nodes,
511 "edges": edges
512 });
513
514 Ok(json_layout.to_string())
515 }
516}