1use std::io::Write;
2
3use crate::common_attributes::{CommonAttributes, implement_common_attributes};
4use crate::escape::escape_xml;
5use crate::{Coordinate, SvgColor, SvgElement, SvgId, SvgStrokeLinecap, SvgTransform};
6
7#[derive(Default)]
8pub struct SvgPath {
9 pub shape: SvgShape,
10 pub stroke: Option<SvgColor>,
11 pub stroke_width: Option<f64>,
12 pub common_attributes: CommonAttributes,
13}
14
15implement_common_attributes!(SvgPath);
16
17enum SvgPathElement {
18 LineAbsolute((Coordinate, Coordinate)),
19 LineRelative((Coordinate, Coordinate)),
20 ArcRelative(
21 (
22 Coordinate,
23 Coordinate,
24 Coordinate,
25 Coordinate,
26 Coordinate,
27 Coordinate,
28 Coordinate,
29 ),
30 ),
31 MoveAbsolute((Coordinate, Coordinate)),
32 MoveRelative((Coordinate, Coordinate)),
33 Close,
39}
40
41impl From<SvgPath> for SvgElement {
42 fn from(value: SvgPath) -> Self {
43 Self::Path(value)
44 }
45}
46
47impl SvgPath {
48 #[allow(clippy::missing_const_for_fn)]
49 pub fn shape(mut self, shape: SvgShape) -> Self {
50 self.shape = shape;
51 self
52 }
53
54 pub const fn stroke_width(mut self, width: f64) -> Self {
55 self.stroke_width = Some(width);
56 self
57 }
58
59 pub const fn stroke(mut self, color: SvgColor) -> Self {
60 self.stroke = Some(color);
61 self
62 }
63
64 pub(crate) fn write<W: Write>(&self, id: Option<SvgId>, writer: &mut W) {
65 #![allow(clippy::unwrap_used)]
66 writer.write_all(b"<path").unwrap();
67 if let Some(id) = id {
68 id.write(writer);
69 }
70 if let Some(stroke) = &self.stroke {
71 stroke.write_stroke(writer);
72 }
73 self.common_attributes.write(writer);
74 if let Some(stroke_width) = &self.stroke_width {
75 writer
76 .write_all(format!(" stroke-width=\"{stroke_width}\"").as_bytes())
77 .unwrap();
78 }
79 writer.write_all(b" d=\"").unwrap();
80 self.shape.write(writer);
81 writer.write_all(b"\"").unwrap();
82 if let Some(title) = &self.common_attributes.title {
83 writer
84 .write_all(format!("><title>{}</title></path>", escape_xml(title)).as_bytes())
85 .unwrap();
86 } else {
87 writer.write_all(b"/>\n").unwrap();
88 }
89 }
90}
91
92#[derive(Default)]
93pub struct SvgShape {
94 elements: Vec<SvgPathElement>,
95}
96
97impl SvgShape {
98 pub const fn new() -> Self {
99 Self {
100 elements: Vec::new(),
101 }
102 }
103
104 pub fn at<C: Into<Coordinate>>(x: C, y: C) -> Self {
105 Self {
106 elements: vec![SvgPathElement::MoveAbsolute((x.into(), y.into()))],
107 }
108 }
109
110 pub const fn is_empty(&self) -> bool {
111 self.elements.is_empty()
113 }
114
115 pub fn line_to_absolute<I: Into<Coordinate>>(mut self, x: I, y: I) -> Self {
116 self.elements
117 .push(SvgPathElement::LineAbsolute((x.into(), y.into())));
118 self
119 }
120
121 pub fn line_to_relative<C: Into<Coordinate>>(mut self, x: C, y: C) -> Self {
122 self.elements
123 .push(SvgPathElement::LineRelative((x.into(), y.into())));
124 self
125 }
126
127 #[allow(clippy::too_many_arguments)]
128 pub fn arc_to_relative<C: Into<Coordinate>>(
129 mut self,
130 radius_x: C,
131 radius_y: C,
132 x_axis_rotation: C,
133 large_arc_flag: C,
134 sweep_flag: C,
135 dx: C,
136 dy: C,
137 ) -> Self {
138 self.elements.push(SvgPathElement::ArcRelative((
139 radius_x.into(),
140 radius_y.into(),
141 x_axis_rotation.into(),
142 large_arc_flag.into(),
143 sweep_flag.into(),
144 dx.into(),
145 dy.into(),
146 )));
147 self
148 }
149
150 pub fn move_to_absolute<C: Into<Coordinate>>(mut self, x: C, y: C) -> Self {
151 self.elements
152 .push(SvgPathElement::MoveAbsolute((x.into(), y.into())));
153 self
154 }
155
156 pub fn move_to_relative(mut self, x: Coordinate, y: Coordinate) -> Self {
157 self.elements.push(SvgPathElement::MoveRelative((x, y)));
158 self
159 }
160
161 pub fn circle_absolute<C: Into<Coordinate>>(self, center_x: C, center_y: C, radius: C) -> Self {
162 let radius = radius.into();
163 self.move_to_absolute(center_x.into() - radius, center_y.into())
168 .arc_to_relative(radius, radius, 0., 1., 0., radius * 2., 0.)
169 .arc_to_relative(radius, radius, 0., 1., 0., -radius * 2., 0.)
170 }
171
172 pub fn close(mut self) -> Self {
173 self.elements.push(SvgPathElement::Close);
174 self
175 }
176
177 pub fn data_string(&self) -> String {
178 #![allow(clippy::unwrap_used)]
179 let mut buffer = Vec::new();
180 self.write(&mut buffer);
181 String::from_utf8(buffer).unwrap()
182 }
183
184 pub(crate) fn write<W: Write>(&self, writer: &mut W) {
185 #![allow(clippy::unwrap_used)]
186 for element in &self.elements {
187 match element {
188 SvgPathElement::MoveAbsolute((x, y)) => {
189 writer.write_all(format!("M {x} {y}").as_bytes()).unwrap();
190 }
191 SvgPathElement::MoveRelative((x, y)) => {
192 writer.write_all(format!("m {x} {y}").as_bytes()).unwrap();
193 }
194 SvgPathElement::LineAbsolute((x, y)) => {
195 writer.write_all(format!("L {x} {y}").as_bytes()).unwrap();
196 }
197 SvgPathElement::LineRelative((x, y)) => {
198 writer.write_all(format!("l {x} {y}").as_bytes()).unwrap();
199 }
200 SvgPathElement::ArcRelative((rx, ry, x_rot, a_flag, s_flag, dx, dy)) => {
201 writer
203 .write_all(
204 format!("a {rx} {ry} {x_rot} {a_flag} {s_flag} {dx} {dy}").as_bytes(),
205 )
206 .unwrap();
207 }
208 SvgPathElement::Close => {
209 writer.write_all(b"Z").unwrap();
210 }
211 }
212 }
213 }
214}