//! Exporter SVG: un [`RenderPlan`] → documento SVG completo. //! //! El mismo painter que dibuja en pantalla (vía el trait `Canvas`) se //! graba con un `PlanRecorder` y el plan resultante se vuelca acá. Un //! solo camino de código para screen y export. //! //! v1: los comandos de clip (`PushClip`/`PopClip`) se ignoran — el //! recorte no es crítico para la mayoría de exports y SVG `clipPath` //! agrega complejidad de IDs. Se puede agregar después sin romper API. use pineal_render::{Color, RenderCmd, RenderPlan}; use std::fmt::Write; /// Convierte un `RenderPlan` a un documento SVG de `width × height`. pub fn to_svg(plan: &RenderPlan, width: f32, height: f32) -> String { let mut s = String::with_capacity(256 + plan.cmds.len() * 80); let _ = write!( s, "" ); for cmd in &plan.cmds { emit_cmd(&mut s, cmd); } s.push_str(""); s } fn emit_cmd(s: &mut String, cmd: &RenderCmd) { match cmd { // v1: clips ignorados (ver doc del módulo). RenderCmd::PushClip(_) | RenderCmd::PopClip => {} RenderCmd::FillRect { rect, color } => { let (c, a) = svg_color(*color); let _ = write!( s, "", rect.x, rect.y, rect.w, rect.h ); } RenderCmd::StrokeRect { rect, stroke } => { let (c, a) = svg_color(stroke.color); let _ = write!( s, "", rect.x, rect.y, rect.w, rect.h, stroke.width ); } RenderCmd::StrokeLine { a: p0, b: p1, stroke } => { let (c, alpha) = svg_color(stroke.color); let _ = write!( s, "", p0.x, p0.y, p1.x, p1.y, stroke.width ); } RenderCmd::StrokePolyline { coords, stroke } => { let (c, alpha) = svg_color(stroke.color); s.push_str("", stroke.width ); } RenderCmd::FillTriangleStrip { coords, colors } => { emit_triangle_strip(s, coords, colors); } RenderCmd::DrawText { p, text, color, size_px } => { let (c, a) = svg_color(*color); let _ = write!( s, "{}", p.x, p.y, escape_xml(text) ); } } } /// Emite `x0,y0 x1,y1 …` para el atributo `points` de polyline/polygon. fn emit_points(s: &mut String, coords: &[f32]) { for (i, pair) in coords.chunks_exact(2).enumerate() { if i > 0 { s.push(' '); } let _ = write!(s, "{},{}", pair[0], pair[1]); } } /// Un triangle strip de N vértices = N-2 triángulos. Cada triángulo se /// emite como `` con el color promedio de sus 3 vértices (SVG /// no tiene gradient por-vértice trivial). fn emit_triangle_strip(s: &mut String, coords: &[f32], colors: &[Color]) { let n = coords.len() / 2; if n < 3 { return; } for t in 0..n - 2 { let (i0, i1, i2) = (t, t + 1, t + 2); let avg = avg_color(&[ colors.get(i0).copied(), colors.get(i1).copied(), colors.get(i2).copied(), ]); let (c, a) = svg_color(avg); let _ = write!( s, "", coords[i0 * 2], coords[i0 * 2 + 1], coords[i1 * 2], coords[i1 * 2 + 1], coords[i2 * 2], coords[i2 * 2 + 1], ); } } fn avg_color(cs: &[Option]) -> Color { let mut acc = Color::rgba(0.0, 0.0, 0.0, 0.0); let mut n = 0.0; for c in cs.iter().flatten() { acc.r += c.r; acc.g += c.g; acc.b += c.b; acc.a += c.a; n += 1.0; } if n == 0.0 { return Color::TRANSPARENT; } Color::rgba(acc.r / n, acc.g / n, acc.b / n, acc.a / n) } /// `Color` f32 → (`rgb(R,G,B)` con enteros 0-255, alpha 0-1). fn svg_color(c: Color) -> (String, f32) { let to255 = |v: f32| (v.clamp(0.0, 1.0) * 255.0).round() as u8; ( format!("rgb({},{},{})", to255(c.r), to255(c.g), to255(c.b)), c.a.clamp(0.0, 1.0), ) } /// Escapa los 5 caracteres especiales de XML en contenido de texto. fn escape_xml(text: &str) -> String { let mut out = String::with_capacity(text.len()); for ch in text.chars() { match ch { '&' => out.push_str("&"), '<' => out.push_str("<"), '>' => out.push_str(">"), '"' => out.push_str("""), '\'' => out.push_str("'"), c => out.push(c), } } out } #[cfg(test)] mod tests { use super::*; use pineal_render::{Canvas, Point, Rect, StrokeStyle}; fn sample_plan() -> RenderPlan { let mut rec = pineal_render::PlanRecorder::new(); rec.fill_rect(Rect::new(1.0, 2.0, 30.0, 40.0), Color::from_hex(0xff0000)); rec.stroke_line( Point::new(0.0, 0.0), Point::new(100.0, 50.0), StrokeStyle::new(2.0, Color::BLACK), ); rec.draw_text(Point::new(5.0, 10.0), "a")); assert!(svg.contains("width=\"200\"")); assert!(svg.contains("viewBox=\"0 0 200 100\"")); } #[test] fn emits_each_primitive() { let svg = to_svg(&sample_plan(), 200.0, 100.0); assert!(svg.contains("