1use geometry_core::{Point, Rect};
2use std::sync::atomic::{AtomicU64, Ordering};
3
4static NEXT_PATH_ID: AtomicU64 = AtomicU64::new(1);
5
6#[derive(Debug, Clone, PartialEq)]
7pub enum PathVerb {
8 MoveTo(Point),
9 LineTo(Point),
10 QuadTo {
11 ctrl: Point,
12 to: Point,
13 },
14 CubicTo {
15 ctrl1: Point,
16 ctrl2: Point,
17 to: Point,
18 },
19 Close,
20}
21
22#[derive(Debug, Clone)]
23pub struct PathData {
24 pub id: u64,
25 pub(crate) verbs: Vec<PathVerb>,
26 bounds_cache: std::sync::OnceLock<Option<Rect>>,
28}
29
30impl PartialEq for PathData {
32 fn eq(&self, other: &Self) -> bool {
33 self.verbs == other.verbs
34 }
35}
36
37impl Default for PathData {
38 fn default() -> Self {
39 Self {
40 id: NEXT_PATH_ID.fetch_add(1, Ordering::Relaxed),
41 verbs: Vec::new(),
42 bounds_cache: std::sync::OnceLock::new(),
43 }
44 }
45}
46
47impl PathData {
48 pub fn new() -> Self {
49 Self::default()
50 }
51
52 pub fn verbs(&self) -> &[PathVerb] {
53 &self.verbs
54 }
55
56 pub fn move_to(mut self, p: Point) -> Self {
57 self.verbs.push(PathVerb::MoveTo(p));
58 self.bounds_cache = std::sync::OnceLock::new();
59 self
60 }
61
62 pub fn line_to(mut self, p: Point) -> Self {
63 self.verbs.push(PathVerb::LineTo(p));
64 self.bounds_cache = std::sync::OnceLock::new();
65 self
66 }
67
68 pub fn quad_to(mut self, ctrl: Point, to: Point) -> Self {
69 self.verbs.push(PathVerb::QuadTo { ctrl, to });
70 self.bounds_cache = std::sync::OnceLock::new();
71 self
72 }
73
74 pub fn cubic_to(mut self, ctrl1: Point, ctrl2: Point, to: Point) -> Self {
75 self.verbs.push(PathVerb::CubicTo { ctrl1, ctrl2, to });
76 self.bounds_cache = std::sync::OnceLock::new();
77 self
78 }
79
80 pub fn close(mut self) -> Self {
81 self.verbs.push(PathVerb::Close);
82 self.bounds_cache = std::sync::OnceLock::new();
83 self
84 }
85
86 pub fn refit(&self, s: f32, dx: f32, dy: f32) -> Self {
90 self.refit_xy(s, s, dx, dy)
91 }
92
93 pub fn refit_xy(&self, sx: f32, sy: f32, dx: f32, dy: f32) -> Self {
95 let map = |p: Point| Point::new(p.x * sx + dx, p.y * sy + dy);
96 let mut out = Self::new();
97 for verb in &self.verbs {
98 out = match verb {
99 PathVerb::MoveTo(p) => out.move_to(map(*p)),
100 PathVerb::LineTo(p) => out.line_to(map(*p)),
101 PathVerb::QuadTo { ctrl, to } => out.quad_to(map(*ctrl), map(*to)),
102 PathVerb::CubicTo { ctrl1, ctrl2, to } => {
103 out.cubic_to(map(*ctrl1), map(*ctrl2), map(*to))
104 }
105 PathVerb::Close => out.close(),
106 };
107 }
108 out
109 }
110
111 pub fn polygon(points: &[Point]) -> Self {
113 let mut path = Self::new();
114 let Some((first, rest)) = points.split_first() else {
115 return path;
116 };
117 path = path.move_to(*first);
118 for p in rest {
119 path = path.line_to(*p);
120 }
121 path.close()
122 }
123
124 pub fn bounds(&self) -> Option<Rect> {
125 *self.bounds_cache.get_or_init(|| {
126 let mut min_x = f32::INFINITY;
127 let mut min_y = f32::INFINITY;
128 let mut max_x = f32::NEG_INFINITY;
129 let mut max_y = f32::NEG_INFINITY;
130 let mut has_geometry = false;
131
132 for verb in &self.verbs {
133 match verb {
134 PathVerb::MoveTo(p) | PathVerb::LineTo(p) => {
135 min_x = min_x.min(p.x);
136 min_y = min_y.min(p.y);
137 max_x = max_x.max(p.x);
138 max_y = max_y.max(p.y);
139 has_geometry = true;
140 }
141 PathVerb::QuadTo { ctrl, to } => {
142 for p in &[ctrl, to] {
144 min_x = min_x.min(p.x);
145 min_y = min_y.min(p.y);
146 max_x = max_x.max(p.x);
147 max_y = max_y.max(p.y);
148 }
149 has_geometry = true;
150 }
151 PathVerb::CubicTo { ctrl1, ctrl2, to } => {
152 for p in &[ctrl1, ctrl2, to] {
153 min_x = min_x.min(p.x);
154 min_y = min_y.min(p.y);
155 max_x = max_x.max(p.x);
156 max_y = max_y.max(p.y);
157 }
158 has_geometry = true;
159 }
160 PathVerb::Close => {}
161 }
162 }
163
164 if has_geometry {
165 Some(Rect::new(min_x, min_y, max_x - min_x, max_y - min_y))
166 } else {
167 None
168 }
169 })
170 }
171}
172
173#[cfg(test)]
174mod tests {
175 use super::*;
176
177 #[test]
178 fn path_data_new_is_empty() {
179 let path = PathData::new();
180 assert!(path.verbs().is_empty());
181 }
182
183 #[test]
184 fn path_data_move_to_adds_verb() {
185 let path = PathData::new().move_to(Point::new(0.0, 0.0));
186 assert_eq!(path.verbs().len(), 1);
187 assert!(matches!(path.verbs()[0], PathVerb::MoveTo(_)));
188 }
189
190 #[test]
191 fn path_data_line_to_adds_verb() {
192 let path = PathData::new()
193 .move_to(Point::new(0.0, 0.0))
194 .line_to(Point::new(1.0, 1.0));
195 assert_eq!(path.verbs().len(), 2);
196 assert!(matches!(path.verbs()[1], PathVerb::LineTo(_)));
197 }
198
199 #[test]
200 fn path_data_close_adds_verb() {
201 let path = PathData::new().move_to(Point::new(0.0, 0.0)).close();
202 assert!(matches!(path.verbs().last().unwrap(), PathVerb::Close));
203 }
204
205 #[test]
206 fn path_data_quad_to_adds_verb() {
207 let path = PathData::new().quad_to(Point::new(1.0, 0.0), Point::new(2.0, 0.0));
208 assert!(matches!(path.verbs()[0], PathVerb::QuadTo { .. }));
209 }
210
211 #[test]
212 fn path_data_cubic_to_adds_verb() {
213 let path = PathData::new().cubic_to(
214 Point::new(1.0, 0.0),
215 Point::new(2.0, 0.0),
216 Point::new(3.0, 0.0),
217 );
218 assert!(matches!(path.verbs()[0], PathVerb::CubicTo { .. }));
219 }
220
221 #[test]
222 fn path_data_builder_accumulates_verbs() {
223 let path = PathData::new()
224 .move_to(Point::new(0.0, 0.0))
225 .line_to(Point::new(1.0, 0.0))
226 .line_to(Point::new(1.0, 1.0))
227 .close();
228 assert_eq!(path.verbs().len(), 4);
229 }
230}