1#![allow(dead_code)]
4
5#[allow(dead_code)]
9#[derive(Debug, Clone)]
10pub enum SvgPathCmd {
11 MoveTo(f32, f32),
12 LineTo(f32, f32),
13 CubicBezier(f32, f32, f32, f32, f32, f32),
14 ClosePath,
15}
16
17#[allow(dead_code)]
19pub struct SvgPathElement {
20 pub commands: Vec<SvgPathCmd>,
21 pub stroke: String,
22 pub stroke_width: f32,
23 pub fill: String,
24}
25
26#[allow(dead_code)]
28pub fn new_svg_path(stroke: &str, stroke_width: f32, fill: &str) -> SvgPathElement {
29 SvgPathElement {
30 commands: Vec::new(),
31 stroke: stroke.to_string(),
32 stroke_width,
33 fill: fill.to_string(),
34 }
35}
36
37#[allow(dead_code)]
39pub fn move_to(path: &mut SvgPathElement, x: f32, y: f32) {
40 path.commands.push(SvgPathCmd::MoveTo(x, y));
41}
42
43#[allow(dead_code)]
45pub fn line_to(path: &mut SvgPathElement, x: f32, y: f32) {
46 path.commands.push(SvgPathCmd::LineTo(x, y));
47}
48
49#[allow(dead_code)]
51pub fn cubic_to(path: &mut SvgPathElement, cx1: f32, cy1: f32, cx2: f32, cy2: f32, x: f32, y: f32) {
52 path.commands
53 .push(SvgPathCmd::CubicBezier(cx1, cy1, cx2, cy2, x, y));
54}
55
56#[allow(dead_code)]
58pub fn close_path(path: &mut SvgPathElement) {
59 path.commands.push(SvgPathCmd::ClosePath);
60}
61
62#[allow(dead_code)]
64pub fn commands_to_d(commands: &[SvgPathCmd]) -> String {
65 commands
66 .iter()
67 .map(|cmd| match cmd {
68 SvgPathCmd::MoveTo(x, y) => format!("M {:.4} {:.4}", x, y),
69 SvgPathCmd::LineTo(x, y) => format!("L {:.4} {:.4}", x, y),
70 SvgPathCmd::CubicBezier(cx1, cy1, cx2, cy2, x, y) => format!(
71 "C {:.4} {:.4} {:.4} {:.4} {:.4} {:.4}",
72 cx1, cy1, cx2, cy2, x, y
73 ),
74 SvgPathCmd::ClosePath => "Z".to_string(),
75 })
76 .collect::<Vec<_>>()
77 .join(" ")
78}
79
80#[allow(dead_code)]
82pub fn path_to_svg_tag(path: &SvgPathElement) -> String {
83 let d = commands_to_d(&path.commands);
84 format!(
85 "<path d=\"{}\" stroke=\"{}\" stroke-width=\"{}\" fill=\"{}\"/>",
86 d, path.stroke, path.stroke_width, path.fill
87 )
88}
89
90#[allow(dead_code)]
92pub fn wrap_svg(width: f32, height: f32, body: &str) -> String {
93 format!(
94 "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"{}\" height=\"{}\">{}</svg>",
95 width, height, body
96 )
97}
98
99#[allow(dead_code)]
101pub fn command_count(path: &SvgPathElement) -> usize {
102 path.commands.len()
103}
104
105#[allow(dead_code)]
107pub fn starts_with_move(path: &SvgPathElement) -> bool {
108 path.commands
109 .first()
110 .is_some_and(|c| matches!(c, SvgPathCmd::MoveTo(..)))
111}
112
113#[allow(dead_code)]
115pub fn polyline_to_path(points: &[[f32; 2]], stroke: &str, stroke_width: f32) -> SvgPathElement {
116 let mut path = new_svg_path(stroke, stroke_width, "none");
117 for (i, &[x, y]) in points.iter().enumerate() {
118 if i == 0 {
119 move_to(&mut path, x, y);
120 } else {
121 line_to(&mut path, x, y);
122 }
123 }
124 path
125}
126
127#[cfg(test)]
128mod tests {
129 use super::*;
130
131 #[test]
132 fn move_to_adds_command() {
133 let mut p = new_svg_path("black", 1.0, "none");
134 move_to(&mut p, 10.0, 20.0);
135 assert_eq!(command_count(&p), 1);
136 }
137
138 #[test]
139 fn commands_to_d_move() {
140 let d = commands_to_d(&[SvgPathCmd::MoveTo(1.0, 2.0)]);
141 assert!(d.contains('M'));
142 }
143
144 #[test]
145 fn commands_to_d_line() {
146 let d = commands_to_d(&[SvgPathCmd::LineTo(3.0, 4.0)]);
147 assert!(d.contains('L'));
148 }
149
150 #[test]
151 fn commands_to_d_close() {
152 let d = commands_to_d(&[SvgPathCmd::ClosePath]);
153 assert!(d.contains('Z'));
154 }
155
156 #[test]
157 fn commands_to_d_cubic() {
158 let d = commands_to_d(&[SvgPathCmd::CubicBezier(1.0, 2.0, 3.0, 4.0, 5.0, 6.0)]);
159 assert!(d.contains('C'));
160 }
161
162 #[test]
163 fn path_tag_has_stroke() {
164 let mut p = new_svg_path("red", 2.0, "none");
165 move_to(&mut p, 0.0, 0.0);
166 let tag = path_to_svg_tag(&p);
167 assert!(tag.contains("red"));
168 }
169
170 #[test]
171 fn wrap_svg_has_namespace() {
172 let svg = wrap_svg(100.0, 200.0, "");
173 assert!(svg.contains("xmlns"));
174 }
175
176 #[test]
177 fn starts_with_move_true() {
178 let mut p = new_svg_path("black", 1.0, "none");
179 move_to(&mut p, 0.0, 0.0);
180 assert!(starts_with_move(&p));
181 }
182
183 #[test]
184 fn starts_with_move_false_empty() {
185 let p = new_svg_path("black", 1.0, "none");
186 assert!(!starts_with_move(&p));
187 }
188
189 #[test]
190 fn polyline_to_path_count() {
191 let pts = vec![[0.0, 0.0], [1.0, 0.0], [1.0, 1.0]];
192 let p = polyline_to_path(&pts, "blue", 1.0);
193 assert_eq!(command_count(&p), 3);
194 }
195}