Skip to main content

svgplot/
path.rs

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    /// The "Close Path" command, called with Z. This command draws a straight line from the current
34    /// position back to the first point of the path. It is often placed at the end of a path node,
35    /// although not always
36    ///
37    /// The SVG syntax for this is 'z' or 'Z'.
38    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        // If it contains the single initial move command.
112        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        // https://www.smashingmagazine.com/2019/03/svg-circle-decomposition-paths/
164        //       M (CX - R), CY
165        //       a R,R 0 1,0 (R * 2),0
166        //       a R,R 0 1,0 -(R * 2),0
167        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                    // a rx ry x-axis-rotation large-arc-flag sweep-flag dx dy
202                    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}