1use 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
10fn 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
19fn num(n: f64) -> String {
21 decimal(n, 3)
22}
23
24pub 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 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}