|
@@ -0,0 +1,217 @@
|
|
|
+use std::fmt::Display;
|
|
|
+use std::io::Write;
|
|
|
+
|
|
|
+/// An SVG document
|
|
|
+pub struct SVG {
|
|
|
+ stuff: Vec<Box<dyn AsSVG>>,
|
|
|
+ size: (f64, f64),
|
|
|
+}
|
|
|
+
|
|
|
+fn xml_tag(w: &mut Vec<u8>, name: &str, attrs: &[(&str, &dyn Display)]) {
|
|
|
+ write!(w, "<{}", name);
|
|
|
+ for (k, v) in attrs {
|
|
|
+ write!(w, " {}=\"{}\"", k, v);
|
|
|
+ }
|
|
|
+ writeln!(w, "/>");
|
|
|
+}
|
|
|
+
|
|
|
+fn xml_open(w: &mut Vec<u8>, name: &str, attrs: &[(&str, &dyn Display)]) {
|
|
|
+ write!(w, "<{}", name);
|
|
|
+ for (k, v) in attrs {
|
|
|
+ write!(w, " {}=\"{}\"", k, v);
|
|
|
+ }
|
|
|
+ writeln!(w, ">");
|
|
|
+}
|
|
|
+
|
|
|
+fn xml_close(w: &mut Vec<u8>, name: &str) {
|
|
|
+ write!(w, "<{}/>", name);
|
|
|
+}
|
|
|
+
|
|
|
+/// 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) {
|
|
|
+ let buf = self.to_bytes();
|
|
|
+ println!("{}", String::from_utf8(buf).unwrap());
|
|
|
+ }
|
|
|
+
|
|
|
+ /// Write this SVG document to a file
|
|
|
+ pub fn to_file<W: Write>(self, w: &mut W) -> Result<(), std::io::Error> {
|
|
|
+ let buf = self.to_bytes();
|
|
|
+ w.write(&buf)?;
|
|
|
+ Ok(())
|
|
|
+ }
|
|
|
+
|
|
|
+ pub fn to_bytes(self) -> Vec<u8> {
|
|
|
+ let mut buf = Vec::new();
|
|
|
+ let (w, h) = self.size;
|
|
|
+ writeln!(buf, "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>");
|
|
|
+ xml_open(
|
|
|
+ &mut buf, "svg",
|
|
|
+ &[("xmlns", &"http://www.w3.org/2000/svg"),
|
|
|
+ ("version", &"1.1"),
|
|
|
+ ("width", &w),
|
|
|
+ ("height", &h),
|
|
|
+ ],
|
|
|
+ );
|
|
|
+ for elem in self.stuff {
|
|
|
+ elem.draw_svg(&mut buf);
|
|
|
+ }
|
|
|
+ xml_close(&mut buf, "svg");
|
|
|
+ buf
|
|
|
+ }
|
|
|
+
|
|
|
+ /// Add a new drawable thing to this SVG document
|
|
|
+ pub fn add<T: AsSVG>(&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 Vec<u8>);
|
|
|
+ // 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<AsSVG>;
|
|
|
+}
|
|
|
+
|
|
|
+/// 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 Vec<u8>) {
|
|
|
+ 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;
|
|
|
+ xml_tag(
|
|
|
+ buf, "circle",
|
|
|
+ &[("cx", &x),
|
|
|
+ ("cy", &y),
|
|
|
+ ("r", &"0.5"),
|
|
|
+ ("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()
|
|
|
+ };
|
|
|
+
|
|
|
+ xml_tag(
|
|
|
+ buf, "path",
|
|
|
+ &[("d", &path), ("stroke", &"black")],
|
|
|
+ );
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ fn consume(self) -> Box<AsSVG> {
|
|
|
+ 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 draw_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 Vec<u8>) {
|
|
|
+ let (x, y) = self.position;
|
|
|
+ let (w, h) = self.size;
|
|
|
+ xml_tag(
|
|
|
+ buf, "rect",
|
|
|
+ &[("x", &x),
|
|
|
+ ("y", &y),
|
|
|
+ ("width", &w),
|
|
|
+ ("height", &h),
|
|
|
+ ("stroke", &"black"),
|
|
|
+ ("fill", &"white"),
|
|
|
+ ]
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ fn consume(self) -> Box<AsSVG> {
|
|
|
+ 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 Vec<u8>) {
|
|
|
+ let (x, y) = self.position;
|
|
|
+ let r = self.radius;
|
|
|
+ xml_tag(
|
|
|
+ buf, "circle",
|
|
|
+ &[("cx", &x),
|
|
|
+ ("cy", &y),
|
|
|
+ ("r", &r),
|
|
|
+ ("stroke", &"black"),
|
|
|
+ ("fill", &"white"),
|
|
|
+ ]
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ fn consume(self) -> Box<AsSVG> {
|
|
|
+ Box::new(self)
|
|
|
+ }
|
|
|
+}
|