Skip to main content

geop_lib_sheet/
svg.rs

1//! A [`Sheet`] as SVG: one group per layer, sized in millimetres.
2
3use std::{convert::Infallible, fmt::Write};
4
5use geop_core_math::solvers::repeat::{Repeated, Step, repeat};
6
7use super::{Anchor, LineType, Shape, Sheet, decimal};
8use crate::xml_escape;
9
10/// How a layer's strokes are dashed, as SVG's `stroke-dasharray`.
11fn dashes(line_type: LineType) -> Option<String> {
12    let dashes = line_type.dashes();
13    (!dashes.is_empty()).then(|| {
14        let lengths: Vec<String> = dashes.iter().map(|d| num(d.abs())).collect();
15        lengths.join(" ")
16    })
17}
18
19/// `n` to a thousandth of a millimetre (see [`decimal`]).
20fn num(n: f64) -> String {
21    decimal(n, 3)
22}
23
24/// The sheet as an SVG document. The paper's `y` points up, SVG's down: every
25/// point is flipped, which turns counter-clockwise arcs into SVG's negative
26/// sweep.
27pub fn to_svg(sheet: &Sheet) -> String {
28    let h = sheet.height;
29    let p = |q: [f64; 2]| format!("{} {}", num(q[0]), num(h - q[1]));
30    let mut out = String::new();
31    let _ = writeln!(
32        out,
33        r#"<svg xmlns="http://www.w3.org/2000/svg" width="{w}mm" height="{hh}mm" viewBox="0 0 {w} {hh}">"#,
34        w = num(sheet.width),
35        hh = num(h)
36    );
37    let _ = writeln!(
38        out,
39        r#"<rect x="0" y="0" width="{}" height="{}" fill="white"/>"#,
40        num(sheet.width),
41        num(h)
42    );
43    for layer in sheet.layers() {
44        let dash = dashes(layer.line_type)
45            .map(|d| format!(r#" stroke-dasharray="{d}""#))
46            .unwrap_or_default();
47        let _ = writeln!(
48            out,
49            r#"<g class="{}" fill="none" stroke="black" stroke-width="{}" stroke-linecap="round"{dash}>"#,
50            layer.name,
51            num(layer.width)
52        );
53        for stroke in sheet.strokes.iter().filter(|s| s.layer == layer) {
54            let _ = match &stroke.shape {
55                Shape::Line(a, b) => writeln!(
56                    out,
57                    r#"<line x1="{}" y1="{}" x2="{}" y2="{}"/>"#,
58                    num(a[0]),
59                    num(h - a[1]),
60                    num(b[0]),
61                    num(h - b[1])
62                ),
63                Shape::Circle { center, radius } => writeln!(
64                    out,
65                    r#"<circle cx="{}" cy="{}" r="{}"/>"#,
66                    num(center[0]),
67                    num(h - center[1]),
68                    num(*radius)
69                ),
70                Shape::Arc {
71                    center,
72                    radius,
73                    start,
74                    end,
75                } => {
76                    // Turned on by whole turns until positive: no more of
77                    // them than the turns it is short by, and one for the
78                    // rounding.
79                    let mut sweep = end - start;
80                    let turns = (-sweep / std::f64::consts::TAU).max(0.0).ceil() as usize;
81                    let run = repeat::<_, Infallible>(turns + 2, || {
82                        Ok(if sweep <= 0.0 {
83                            sweep += std::f64::consts::TAU;
84                            Step::Again
85                        } else {
86                            Step::Done(())
87                        })
88                    });
89                    assert!(
90                        matches!(run, Ok(Repeated::Done { .. })),
91                        "an arc's sweep becomes positive within its turns: {sweep:?}"
92                    );
93                    let at = |a: f64| [center[0] + radius * a.cos(), center[1] + radius * a.sin()];
94                    let large = if sweep > std::f64::consts::PI { 1 } else { 0 };
95                    writeln!(
96                        out,
97                        r#"<path d="M {} A {r} {r} 0 {large} 0 {}"/>"#,
98                        p(at(*start)),
99                        p(at(*end)),
100                        r = num(*radius)
101                    )
102                }
103                Shape::Polyline(points) => writeln!(
104                    out,
105                    r#"<polyline points="{}"/>"#,
106                    points.iter().map(|q| p(*q)).collect::<Vec<_>>().join(" ")
107                ),
108                Shape::Filled(points) => writeln!(
109                    out,
110                    r#"<polygon points="{}" fill="black" stroke="none"/>"#,
111                    points.iter().map(|q| p(*q)).collect::<Vec<_>>().join(" ")
112                ),
113            };
114        }
115        for label in sheet.labels.iter().filter(|l| l.layer == layer) {
116            let anchor = match label.anchor {
117                Anchor::Start => "start",
118                Anchor::Middle => "middle",
119                Anchor::End => "end",
120            };
121            let (x, y) = (num(label.at[0]), num(h - label.at[1]));
122            let rotate = if label.angle == 0.0 {
123                String::new()
124            } else {
125                format!(r#" transform="rotate({} {x} {y})""#, num(-label.angle))
126            };
127            let _ = writeln!(
128                out,
129                r#"<text x="{x}" y="{y}" font-family="sans-serif" font-size="{}" text-anchor="{anchor}" fill="black" stroke="none"{rotate}>{}</text>"#,
130                num(label.height),
131                xml_escape(&label.text)
132            );
133        }
134        out.push_str("</g>\n");
135    }
136    out.push_str("</svg>\n");
137    out
138}