1use crate::geometry::Point;
2
3#[derive(Debug, Clone, Copy, PartialEq)]
5pub struct CubicSegment {
6 pub from: Point,
8 pub ctrl1: Point,
10 pub ctrl2: Point,
12 pub to: Point,
14}
15
16#[derive(Debug, Clone, Copy, PartialEq)]
18pub enum PathCommand {
19 MoveTo(Point),
21 LineTo(Point),
23 CubicTo {
25 ctrl1: Point,
27 ctrl2: Point,
29 to: Point,
31 },
32 Close,
34}
35
36#[derive(Debug, Clone, PartialEq, Default)]
38pub struct SmoothPath {
39 commands: Vec<PathCommand>,
40}
41
42impl SmoothPath {
43 #[must_use]
45 pub fn new() -> Self {
46 Self::default()
47 }
48
49 #[must_use]
51 pub fn commands(&self) -> &[PathCommand] {
52 &self.commands
53 }
54
55 #[must_use]
57 pub fn cubics(&self) -> Vec<CubicSegment> {
58 let mut cubics = Vec::new();
59 let mut current = None;
60 let mut subpath_start = None;
61
62 for command in &self.commands {
63 match *command {
64 PathCommand::MoveTo(point) => {
65 current = Some(point);
66 subpath_start = Some(point);
67 }
68 PathCommand::LineTo(point) => {
69 current = Some(point);
70 }
71 PathCommand::CubicTo { ctrl1, ctrl2, to } => {
72 if let Some(from) = current {
73 cubics.push(CubicSegment {
74 from,
75 ctrl1,
76 ctrl2,
77 to,
78 });
79 }
80 current = Some(to);
81 }
82 PathCommand::Close => {
83 current = subpath_start;
84 }
85 }
86 }
87
88 cubics
89 }
90
91 pub fn move_to(&mut self, point: Point) {
93 self.commands.push(PathCommand::MoveTo(point));
94 }
95
96 pub fn line_to(&mut self, point: Point) {
98 self.commands.push(PathCommand::LineTo(point));
99 }
100
101 pub fn cubic_to(&mut self, ctrl1: Point, ctrl2: Point, to: Point) {
103 self.commands
104 .push(PathCommand::CubicTo { ctrl1, ctrl2, to });
105 }
106
107 pub fn close(&mut self) {
109 self.commands.push(PathCommand::Close);
110 }
111
112 #[must_use]
114 pub fn to_svg_path(&self) -> String {
115 self.to_svg_path_with_precision(6)
116 }
117
118 #[must_use]
120 pub fn to_svg_path_with_precision(&self, precision: usize) -> String {
121 let mut parts = Vec::with_capacity(self.commands.len());
122 for command in &self.commands {
123 match *command {
124 PathCommand::MoveTo(point) => {
125 parts.push(format!("M {}", format_point(point, precision)));
126 }
127 PathCommand::LineTo(point) => {
128 parts.push(format!("L {}", format_point(point, precision)));
129 }
130 PathCommand::CubicTo { ctrl1, ctrl2, to } => {
131 parts.push(format!(
132 "C {} {} {}",
133 format_point(ctrl1, precision),
134 format_point(ctrl2, precision),
135 format_point(to, precision)
136 ));
137 }
138 PathCommand::Close => parts.push("Z".to_owned()),
139 }
140 }
141 parts.join(" ")
142 }
143}
144
145fn format_point(point: Point, precision: usize) -> String {
146 format!(
147 "{},{}",
148 format_number(point.x, precision),
149 format_number(point.y, precision)
150 )
151}
152
153fn format_number(value: f64, precision: usize) -> String {
154 let zero_epsilon = 10.0_f64.powi(-(precision as i32 + 1));
155 let value = if value.abs() < zero_epsilon {
156 0.0
157 } else {
158 value
159 };
160 let mut text = format!("{value:.precision$}");
161 if text.contains('.') {
162 while text.ends_with('0') {
163 text.pop();
164 }
165 if text.ends_with('.') {
166 text.pop();
167 }
168 }
169 if text == "-0" { "0".to_owned() } else { text }
170}