1use crate::core::RenderData;
6use crate::plots::Figure;
7use base64::Engine;
8use image::ImageEncoder;
9use std::fmt::Write;
10use std::path::Path;
11
12pub struct VectorExporter {
14 settings: VectorExportSettings,
16}
17
18#[derive(Debug, Clone)]
20pub struct VectorExportSettings {
21 pub width: f32,
23 pub height: f32,
25 pub background_color: [f32; 4],
27 pub stroke_width: f32,
29 pub include_metadata: bool,
31 pub anti_aliasing: bool,
33}
34
35#[derive(Debug, Clone, Copy, PartialEq)]
37pub enum VectorFormat {
38 Svg,
39 Pdf,
40 Eps,
41}
42
43impl Default for VectorExportSettings {
44 fn default() -> Self {
45 Self {
46 width: 800.0,
47 height: 600.0,
48 background_color: [1.0, 1.0, 1.0, 1.0], stroke_width: 1.0,
50 include_metadata: true,
51 anti_aliasing: true,
52 }
53 }
54}
55
56impl VectorExporter {
57 pub fn new() -> Self {
59 Self {
60 settings: VectorExportSettings::default(),
61 }
62 }
63
64 pub fn with_settings(settings: VectorExportSettings) -> Self {
66 Self { settings }
67 }
68
69 pub fn export_svg<P: AsRef<Path>>(&self, figure: &mut Figure, path: P) -> Result<(), String> {
71 let svg_content = self.render_to_svg(figure)?;
72 std::fs::write(path, svg_content).map_err(|e| format!("Failed to write SVG file: {e}"))?;
73 log::debug!(target: "runmat_plot", "svg export completed");
74 Ok(())
75 }
76
77 pub fn export_pdf<P: AsRef<Path>>(&self, _figure: &mut Figure, _path: P) -> Result<(), String> {
79 Err("PDF export not yet implemented".to_string())
81 }
82
83 pub fn render_to_svg(&self, figure: &mut Figure) -> Result<String, String> {
85 log::debug!(target: "runmat_plot", "svg export render start");
86
87 let mut svg = String::new();
88
89 writeln!(
91 &mut svg,
92 r#"<?xml version="1.0" encoding="UTF-8"?>
93<svg width="{}" height="{}" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">"#,
94 self.settings.width, self.settings.height
95 ).map_err(|e| format!("SVG write error: {e}"))?;
96
97 if self.settings.background_color[3] > 0.0 {
99 writeln!(
100 &mut svg,
101 r#" <rect width="100%" height="100%" fill="{}"/>"#,
102 self.color_to_hex(&self.settings.background_color)
103 )
104 .map_err(|e| format!("SVG write error: {e}"))?;
105 }
106
107 if self.settings.include_metadata {
109 writeln!(
110 &mut svg,
111 " <metadata>\n <rdf:RDF xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\">\n <rdf:Description>\n <dc:creator xmlns:dc=\"http://purl.org/dc/elements/1.1/\">RunMat Plot System</dc:creator>\n </rdf:Description>\n </rdf:RDF>\n </metadata>"
112 ).map_err(|e| format!("SVG write error: {e}"))?;
113 }
114
115 if let Some(title) = &figure.title {
117 let fs = 18;
118 writeln!(
119 &mut svg,
120 " <text x=\"{}\" y=\"{}\" text-anchor=\"middle\" font-size=\"{}\" fill=\"#000000\" font-family=\"sans-serif\">{}</text>",
121 self.settings.width * 0.5,
122 24,
123 fs,
124 xml_escape(title)
125 ).map_err(|e| format!("SVG write error: {e}"))?;
126 }
127
128 let (rows, cols) = figure.axes_grid();
130 let gaps = (8.0f32, 8.0f32);
131 let (hgap, vgap) = gaps;
132 let total_hgap = hgap * (cols.saturating_sub(1) as f32);
133 let total_vgap = vgap * (rows.saturating_sub(1) as f32);
134 let cell_w = (self.settings.width - total_hgap).max(1.0) / (cols.max(1) as f32);
135 let cell_h = (self.settings.height - total_vgap).max(1.0) / (rows.max(1) as f32);
136
137 let axes_vps: Vec<(f32, f32, f32, f32)> = (0..rows)
138 .flat_map(|r| {
139 (0..cols).map(move |c| {
140 (
141 c as f32 * (cell_w + hgap),
142 r as f32 * (cell_h + vgap),
143 cell_w,
144 cell_h,
145 )
146 })
147 })
148 .collect();
149
150 let axes_map = figure.plot_axes_indices().to_vec();
151 let rds = figure.render_data();
152 for (i, rd) in rds.iter().enumerate() {
153 let ax = axes_map.get(i).copied().unwrap_or(0).min(rows * cols - 1);
154 let vp = axes_vps[ax];
155 if let Some(lbl) = &figure.x_label {
157 let cx = vp.0 + vp.2 * 0.5;
158 let cy = vp.1 + vp.3 + 20.0;
159 writeln!(
160 &mut svg,
161 " <text x=\"{}\" y=\"{}\" text-anchor=\"middle\" font-size=\"12\" fill=\"#000000\" font-family=\"sans-serif\">{}</text>",
162 cx, cy, xml_escape(lbl)
163 ).map_err(|e| format!("SVG write error: {e}"))?;
164 }
165 if let Some(lbl) = &figure.y_label {
166 let cx = vp.0 - 24.0;
167 let cy = vp.1 + vp.3 * 0.5;
168 writeln!(
169 &mut svg,
170 " <text x=\"{}\" y=\"{}\" transform=\"rotate(-90, {}, {})\" text-anchor=\"middle\" font-size=\"12\" fill=\"#000000\" font-family=\"sans-serif\">{}</text>",
171 cx, cy, cx, cy, xml_escape(lbl)
172 ).map_err(|e| format!("SVG write error: {e}"))?;
173 }
174 for pie_label in figure.pie_labels_for_axes(ax) {
175 let radius = vp.2.min(vp.3) * 0.4;
176 let screen_x = vp.0 + vp.2 * 0.5 + pie_label.position.x * radius;
177 let screen_y = vp.1 + vp.3 * 0.5 - pie_label.position.y * radius;
178 writeln!(
179 &mut svg,
180 " <text x=\"{}\" y=\"{}\" text-anchor=\"middle\" font-size=\"12\" fill=\"#000000\" font-family=\"sans-serif\">{}</text>",
181 screen_x,
182 screen_y,
183 xml_escape(&pie_label.label)
184 ).map_err(|e| format!("SVG write error: {e}"))?;
185 }
186 self.add_render_data_to_svg_viewport(&mut svg, rd, vp)?;
187 }
188
189 writeln!(&mut svg, "</svg>").map_err(|e| format!("SVG write error: {e}"))?;
191
192 log::debug!(target: "runmat_plot", "svg export size chars={}", svg.len());
193 Ok(svg)
194 }
195
196 #[allow(dead_code)]
199 fn add_render_data_to_svg_viewport(
200 &self,
201 svg: &mut String,
202 render_data: &RenderData,
203 viewport: (f32, f32, f32, f32),
204 ) -> Result<(), String> {
205 match render_data.pipeline_type {
206 crate::core::PipelineType::Lines => {
207 self.add_lines_to_svg_viewport(svg, render_data, viewport)?;
208 }
209 crate::core::PipelineType::Points => {
210 self.add_points_to_svg_viewport(svg, render_data, viewport)?;
211 }
212 crate::core::PipelineType::Triangles => {
213 self.add_triangles_to_svg_viewport(svg, render_data, viewport)?;
214 }
215 crate::core::PipelineType::Textured => {
216 self.add_textured_to_svg_viewport(svg, render_data, viewport)?;
217 }
218 crate::core::PipelineType::Scatter3 => {
219 self.add_points_to_svg_viewport(svg, render_data, viewport)?;
220 }
221 }
222 Ok(())
223 }
224
225 #[allow(dead_code)]
227 fn add_lines_to_svg_viewport(
228 &self,
229 svg: &mut String,
230 render_data: &RenderData,
231 vp: (f32, f32, f32, f32),
232 ) -> Result<(), String> {
233 if render_data.vertices.len() < 2 {
234 return Ok(());
235 }
236
237 writeln!(svg, " <g>").map_err(|e| format!("SVG write error: {e}"))?;
239
240 let lw = render_data.material.roughness.max(0.5);
242 let style_code = render_data.material.metallic as i32;
243 let cap_code = render_data.material.emissive.x as i32;
244 let join_code = render_data.material.emissive.y as i32;
245 let stroke = self.color_to_hex(
246 &render_data
247 .vertices
248 .first()
249 .map(|v| v.color)
250 .unwrap_or([0.0, 0.0, 0.0, 1.0]),
251 );
252 let stroke_linecap = match cap_code {
253 1 => "square",
254 2 => "round",
255 _ => "butt",
256 };
257 let stroke_linejoin = match join_code {
258 1 => "bevel",
259 2 => "round",
260 _ => "miter",
261 };
262 let stroke_dasharray: Option<String> = match style_code {
268 1 => Some(format!("{},{}", 6.0 * lw, 6.0 * lw)),
269 2 => Some(format!("{},{}", 1.0 * lw, 6.0 * lw)),
270 3 => Some(format!(
271 "{},{},{},{}",
272 6.0 * lw,
273 4.0 * lw,
274 1.0 * lw,
275 4.0 * lw
276 )),
277 _ => None,
278 };
279
280 for chunk in render_data.vertices.chunks(2) {
281 if chunk.len() == 2 {
282 let start = &chunk[0];
283 let end = &chunk[1];
284
285 let start_screen = self.world_to_screen_viewport(start.position, vp);
286 let end_screen = self.world_to_screen_viewport(end.position, vp);
287
288 if let Some(ref dash) = stroke_dasharray {
289 writeln!(
290 svg,
291 r#" <line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{}" stroke-width="{}" stroke-linecap="{}" stroke-linejoin="{}" stroke-dasharray="{}"/>"#,
292 start_screen[0], start_screen[1], end_screen[0], end_screen[1], stroke, lw, stroke_linecap, stroke_linejoin, dash
293 ).map_err(|e| format!("SVG write error: {e}"))?;
294 } else {
295 writeln!(
296 svg,
297 r#" <line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{}" stroke-width="{}" stroke-linecap="{}" stroke-linejoin="{}"/>"#,
298 start_screen[0], start_screen[1], end_screen[0], end_screen[1], stroke, lw, stroke_linecap, stroke_linejoin
299 ).map_err(|e| format!("SVG write error: {e}"))?;
300 }
301 }
302 }
303
304 writeln!(svg, " </g>").map_err(|e| format!("SVG write error: {e}"))?;
305 Ok(())
306 }
307
308 #[allow(dead_code)]
310 fn add_points_to_svg_viewport(
311 &self,
312 svg: &mut String,
313 render_data: &RenderData,
314 vp: (f32, f32, f32, f32),
315 ) -> Result<(), String> {
316 writeln!(svg, " <g>").map_err(|e| format!("SVG write error: {e}"))?;
317
318 let marker_shape = render_data.material.metallic as u32; let edge_color = render_data.material.emissive;
320 let edge_width = render_data.material.roughness.max(0.0);
321 for vertex in &render_data.vertices {
322 let screen_pos = self.world_to_screen_viewport(vertex.position, vp);
323 let radius = (vertex.normal[2] * 0.5).max(1.0);
324 if marker_shape == 1u32 {
327 let x = screen_pos[0] - radius;
329 let y = screen_pos[1] - radius;
330 writeln!(
331 svg,
332 r#" <rect x="{}" y="{}" width="{}" height="{}" fill="{}" stroke="{}" stroke-width="{}"/>"#,
333 x,
334 y,
335 radius*2.0,
336 radius*2.0,
337 self.color_to_hex(&vertex.color),
338 self.color_to_hex(&edge_color.to_array()),
339 edge_width
340 ).map_err(|e| format!("SVG write error: {e}"))?;
341 } else {
342 writeln!(
344 svg,
345 r#" <circle cx="{}" cy="{}" r="{}" fill="{}" stroke="{}" stroke-width="{}"/>"#,
346 screen_pos[0],
347 screen_pos[1],
348 radius,
349 self.color_to_hex(&vertex.color),
350 self.color_to_hex(&edge_color.to_array()),
351 edge_width
352 ).map_err(|e| format!("SVG write error: {e}"))?;
353 }
354 }
355
356 writeln!(svg, " </g>").map_err(|e| format!("SVG write error: {e}"))?;
357 Ok(())
358 }
359
360 #[allow(dead_code)]
362 fn add_triangles_to_svg_viewport(
363 &self,
364 svg: &mut String,
365 render_data: &RenderData,
366 vp: (f32, f32, f32, f32),
367 ) -> Result<(), String> {
368 writeln!(svg, " <g>").map_err(|e| format!("SVG write error: {e}"))?;
369
370 for triangle in render_data.vertices.chunks(3) {
371 if triangle.len() == 3 {
372 let p1 = self.world_to_screen_viewport(triangle[0].position, vp);
373 let p2 = self.world_to_screen_viewport(triangle[1].position, vp);
374 let p3 = self.world_to_screen_viewport(triangle[2].position, vp);
375
376 writeln!(
377 svg,
378 r#" <polygon points="{},{} {},{} {},{}" fill="{}"/>"#,
379 p1[0],
380 p1[1],
381 p2[0],
382 p2[1],
383 p3[0],
384 p3[1],
385 self.color_to_hex(&triangle[0].color)
386 )
387 .map_err(|e| format!("SVG write error: {e}"))?;
388 }
389 }
390
391 writeln!(svg, " </g>").map_err(|e| format!("SVG write error: {e}"))?;
392 Ok(())
393 }
394
395 #[allow(dead_code)]
397 fn world_to_screen_viewport(&self, world_pos: [f32; 3], vp: (f32, f32, f32, f32)) -> [f32; 2] {
398 let (vx, vy, vw, vh) = vp;
399 [
400 vx + (world_pos[0] + 1.0) * 0.5 * vw,
401 vy + (1.0 - (world_pos[1] + 1.0) * 0.5) * vh,
402 ]
403 }
404
405 fn add_textured_to_svg_viewport(
407 &self,
408 svg: &mut String,
409 render_data: &RenderData,
410 vp: (f32, f32, f32, f32),
411 ) -> Result<(), String> {
412 if render_data.vertices.is_empty() {
414 return Ok(());
415 }
416 let mut min_x = f32::INFINITY;
417 let mut min_y = f32::INFINITY;
418 let mut max_x = f32::NEG_INFINITY;
419 let mut max_y = f32::NEG_INFINITY;
420 for v in &render_data.vertices {
421 let p = self.world_to_screen_viewport(v.position, vp);
422 min_x = min_x.min(p[0]);
423 max_x = max_x.max(p[0]);
424 min_y = min_y.min(p[1]);
425 max_y = max_y.max(p[1]);
426 }
427 let w = (max_x - min_x).max(1.0);
428 let h = (max_y - min_y).max(1.0);
429 if let Some(crate::core::scene::ImageData::Rgba8 {
431 width,
432 height,
433 data,
434 }) = &render_data.image
435 {
436 if !data.is_empty() {
437 let mut png_buf: Vec<u8> = Vec::new();
439 {
440 let encoder = image::codecs::png::PngEncoder::new(&mut png_buf);
441 encoder
442 .write_image(data, *width, *height, image::ColorType::Rgba8)
443 .map_err(|e| format!("PNG encode failed: {e}"))?;
444 }
445 let b64 = base64::engine::general_purpose::STANDARD.encode(&png_buf);
446 let href = format!("data:image/png;base64,{}", b64);
447 writeln!(
448 svg,
449 r#" <image x="{}" y="{}" width="{}" height="{}" xlink:href="{}" preserveAspectRatio="none"/>"#,
450 min_x, min_y, w, h, href
451 ).map_err(|e| format!("SVG write error: {e}"))?;
452 return Ok(());
453 }
454 }
455 writeln!(
457 svg,
458 " <rect x=\"{}\" y=\"{}\" width=\"{}\" height=\"{}\" fill=\"#999999\"/>",
459 min_x, min_y, w, h
460 )
461 .map_err(|e| format!("SVG write error: {e}"))?;
462 Ok(())
463 }
464
465 fn color_to_hex(&self, color: &[f32; 4]) -> String {
467 format!(
468 "#{:02x}{:02x}{:02x}",
469 (color[0] * 255.0) as u8,
470 (color[1] * 255.0) as u8,
471 (color[2] * 255.0) as u8
472 )
473 }
474
475 pub fn set_settings(&mut self, settings: VectorExportSettings) {
477 self.settings = settings;
478 }
479
480 pub fn settings(&self) -> &VectorExportSettings {
482 &self.settings
483 }
484}
485fn xml_escape(s: &str) -> String {
486 s.replace('&', "&")
487 .replace('<', "<")
488 .replace('>', ">")
489 .replace('"', """)
490 .replace('\'', "'")
491}
492
493impl Default for VectorExporter {
494 fn default() -> Self {
495 Self::new()
496 }
497}