use std::fmt::{self, Display, Formatter}; use std::fs::{self, OpenOptions}; use std::io::{self, Write}; use std::path::Path; mod xml; use xml::XMLWriter; const MAX_OUTPUT_FILES: u32 = 500; /// An SVG document, which has a fixed size (computed in inches) as /// well as a collection of drawn figures. pub struct SVG { stuff: Vec>, size: (f64, f64), } #[derive(Copy, Clone)] struct Inches { amt: f64 } impl Display for Inches { fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> { self.amt.fmt(f)?; write!(f, "in") } } fn inches(amt: f64) -> Inches { Inches { amt } } /// Create a new empty SVG document of the specified width and height pub fn svg(w: f64, h: f64) -> SVG { SVG { stuff: Vec::new(), size: (w, h), } } impl SVG { /// Print this SVG document to stdout pub fn to_stdout(self) -> io::Result<()> { self.write_svg(&mut std::io::stdout()) } pub fn to_bytes(self) -> io::Result> { let mut buf = Vec::new(); self.write_svg(&mut buf)?; Ok(buf) } /// Print this SVG document to stdout pub fn output(self, p: &str) -> io::Result<()> { let output_dir = Path::new("output"); if !output_dir.is_dir() { fs::create_dir(output_dir)?; } let mut file = { let mut n = 0; let mut path = format!("output/{}{:05}.svg", p, n); while Path::new(&path).exists() { if n > MAX_OUTPUT_FILES { return Err(io::Error::new( io::ErrorKind::Other, "gunpowder_treason: too many output files", )); } n += 1; path = format!("output/{}{:05}.svg", p, n); } eprintln!("writing to {}", path); OpenOptions::new().write(true).create_new(true).open(&path)? }; self.write_svg(&mut file) } pub fn write_svg(self, buf: &mut W) -> io::Result<()> { let (w, h) = self.size; let mut xml = XMLWriter::start(buf)?; xml.block( "svg", &[("xmlns", &"http://www.w3.org/2000/svg"), ("version", &"1.1"), ("width", &inches(w)), ("height", &inches(h)), ("viewBox", &format!("0 0 {} {}", w, h)), ("stroke-width", &"0.0001in"), ], |mut b| { for elem in self.stuff { elem.draw_svg(&mut b)?; } Ok(()) } ) } /// Add a new drawable thing to this SVG document pub fn add(&mut self, t: T) { self.stuff.push(t.consume()) } } /// The AsSVG trait represents things which can be rendered to an SVG /// canvas. pub trait AsSVG { fn draw_svg(&self, buf: &mut XMLWriter) -> io::Result<()>; // This is a bit of a hack to make sure that all of our types live // long enough: it's now on the implementer of AsSVG to box it up // and make sure that we can keep a trait object around. fn consume(self) -> Box; } /// A Line is a sequence of points. It'll be represented as a single /// dot if it has only one point, and otherwise will be drawn as a /// line between them. pub struct Line { start: (f64, f64), points: Vec<(f64, f64)>, } impl AsSVG for Line { fn draw_svg(&self, buf: &mut XMLWriter) -> io::Result<()> { if self.points.len() == 0 { // if there are no later points, then we just draw a // 'point' at the starting position, which is just a tiny // circle let (x, y) = self.start; buf.tag("circle", &[("cx", &x), ("cy", &y), ("r", &"0.01"), ("fill", &"black"), ], ) } else { // Otherwise, we draw a path, which mean assembling this // somewhat wonky path field let path = { let mut path = Vec::new(); let (x, y) = self.start; write!(&mut path, "M{} {}", x, y)?; for &(x, y) in self.points.iter() { write!(&mut path, " L{} {}", x, y)?; } String::from_utf8(path).unwrap() }; buf.tag("path", &[("d", &path), ("stroke", &"black"), ("fill", &"none"), ], ) } } fn consume(self) -> Box { Box::new(self) } } /// Create a new line at the given starting point pub fn line(x: f64, y: f64) -> Line { Line::new(x, y) } impl Line { /// Create a new line at the given starting point pub fn new(x: f64, y: f64) -> Line { Line { start: (x, y), points: Vec::new(), } } /// Draw a line segment from this point to another point pub fn to(mut self, x: f64, y: f64) -> Line { self.points.push((x, y)); self } } pub struct Rect { position: (f64, f64), size: (f64, f64), } pub fn rect(position: (f64, f64), size: (f64, f64)) -> Rect { Rect { position, size } } impl AsSVG for Rect { fn draw_svg(&self, buf: &mut XMLWriter) -> io::Result<()> { let (x, y) = self.position; let (w, h) = self.size; buf.tag("rect", &[("x", &x), ("y", &y), ("width", &w), ("height", &h), ("stroke", &"black"), ("fill", &"none"), ] ) } fn consume(self) -> Box { Box::new(self) } } pub struct Circle { position: (f64, f64), radius: f64, } pub fn circle(position: (f64, f64), radius: f64) -> Circle { Circle { position, radius } } impl AsSVG for Circle { fn draw_svg(&self, buf: &mut XMLWriter) -> io::Result<()> { let (x, y) = self.position; let r = self.radius; buf.tag( "circle", &[("cx", &x), ("cy", &y), ("r", &r), ("stroke", &"black"), ("fill", &"none"), ] ) } fn consume(self) -> Box { Box::new(self) } }