1pub mod boolean;
20
21use glam::Vec2;
22
23use crate::color::Color;
24use crate::scene::graphics::{Fill, Graphics};
25
26#[derive(Debug, Clone, Copy, PartialEq)]
29#[non_exhaustive]
30pub enum PathCommand {
31 MoveTo(Vec2),
33 LineTo(Vec2),
35 QuadTo {
38 control: Vec2,
40 end: Vec2,
42 },
43 CubicTo {
46 c1: Vec2,
48 c2: Vec2,
50 end: Vec2,
52 },
53 Close,
55}
56
57#[derive(Debug, Clone, Default)]
59pub struct PathBuilder {
60 commands: Vec<PathCommand>,
61}
62
63impl PathBuilder {
64 #[must_use]
66 pub fn new() -> Self {
67 Self::default()
68 }
69
70 #[must_use]
72 pub fn move_to(mut self, p: Vec2) -> Self {
73 self.commands.push(PathCommand::MoveTo(p));
74 self
75 }
76
77 #[must_use]
79 pub fn line_to(mut self, p: Vec2) -> Self {
80 self.commands.push(PathCommand::LineTo(p));
81 self
82 }
83
84 #[must_use]
86 pub fn quad_to(mut self, control: Vec2, end: Vec2) -> Self {
87 self.commands.push(PathCommand::QuadTo { control, end });
88 self
89 }
90
91 #[must_use]
93 pub fn cubic_to(mut self, c1: Vec2, c2: Vec2, end: Vec2) -> Self {
94 self.commands.push(PathCommand::CubicTo { c1, c2, end });
95 self
96 }
97
98 #[must_use]
100 pub fn close(mut self) -> Self {
101 self.commands.push(PathCommand::Close);
102 self
103 }
104
105 #[must_use]
107 pub fn build(self) -> Path {
108 Path {
109 commands: self.commands,
110 }
111 }
112}
113
114#[derive(Debug, Clone, PartialEq)]
116pub struct Path {
117 commands: Vec<PathCommand>,
118}
119
120impl Path {
121 #[must_use]
124 pub fn from_commands(commands: Vec<PathCommand>) -> Self {
125 Self { commands }
126 }
127
128 #[must_use]
130 pub fn commands(&self) -> &[PathCommand] {
131 &self.commands
132 }
133
134 #[must_use]
150 pub fn flatten_subpaths(&self, tolerance: f32) -> Vec<Vec<Vec2>> {
151 let mut subs: Vec<Vec<Vec2>> = Vec::new();
152 let mut current: Option<Vec<Vec2>> = None;
153 let mut subpath_start: Vec2 = Vec2::ZERO;
154 let mut last_point: Vec2 = Vec2::ZERO;
155
156 for cmd in &self.commands {
157 match *cmd {
158 PathCommand::MoveTo(p) => {
159 if let Some(v) = current.take()
160 && !v.is_empty()
161 {
162 subs.push(v);
163 }
164 current = Some(vec![p]);
165 subpath_start = p;
166 last_point = p;
167 }
168 PathCommand::LineTo(p) => {
169 if let Some(v) = current.as_mut() {
170 v.push(p);
171 }
172 last_point = p;
173 }
174 PathCommand::QuadTo { control, end } => {
175 if let Some(v) = current.as_mut() {
176 flatten_quad(last_point, control, end, tolerance, v);
177 }
178 last_point = end;
179 }
180 PathCommand::CubicTo { c1, c2, end } => {
181 if let Some(v) = current.as_mut() {
182 flatten_cubic(last_point, c1, c2, end, tolerance, v);
183 }
184 last_point = end;
185 }
186 PathCommand::Close => {
187 if let Some(v) = current.as_mut() {
188 v.push(subpath_start);
189 }
190 last_point = subpath_start;
191 }
192 }
193 }
194 if let Some(v) = current
195 && !v.is_empty()
196 {
197 subs.push(v);
198 }
199 subs
200 }
201
202 #[must_use]
215 pub fn flatten(&self, tolerance: f32) -> Vec<Vec2> {
216 let mut out: Vec<Vec2> = Vec::new();
217 let mut current: Vec2 = Vec2::ZERO;
218 let mut subpath_start: Vec2 = Vec2::ZERO;
219
220 for cmd in &self.commands {
221 match *cmd {
222 PathCommand::MoveTo(p) => {
223 out.push(p);
224 current = p;
225 subpath_start = p;
226 }
227 PathCommand::LineTo(p) => {
228 out.push(p);
229 current = p;
230 }
231 PathCommand::QuadTo { control, end } => {
232 flatten_quad(current, control, end, tolerance, &mut out);
233 current = end;
234 }
235 PathCommand::CubicTo { c1, c2, end } => {
236 flatten_cubic(current, c1, c2, end, tolerance, &mut out);
237 current = end;
238 }
239 PathCommand::Close => {
240 out.push(subpath_start);
241 current = subpath_start;
242 }
243 }
244 }
245 out
246 }
247
248 #[must_use]
253 pub fn stroke_to_graphics(&self, width: f32, color: Color, tolerance: f32) -> Graphics {
254 let pts = self.flatten(tolerance);
255 let mut g = Graphics::new();
256 if pts.len() < 2 {
257 return g;
258 }
259 g.fill(Fill::Solid(color));
263 for pair in pts.windows(2) {
264 g.draw_line(pair[0], pair[1], width);
265 }
266 g
267 }
268
269 #[must_use]
275 pub fn to_mask_polygon(&self, tolerance: f32) -> Vec<Vec2> {
276 self.flatten(tolerance)
277 }
278}
279
280fn flatten_quad(p0: Vec2, p1: Vec2, p2: Vec2, tolerance: f32, out: &mut Vec<Vec2>) {
283 if perp_distance(p1, p0, p2) <= tolerance {
284 out.push(p2);
285 return;
286 }
287 let p01 = (p0 + p1) * 0.5;
288 let p12 = (p1 + p2) * 0.5;
289 let mid = (p01 + p12) * 0.5;
290 flatten_quad(p0, p01, mid, tolerance, out);
291 flatten_quad(mid, p12, p2, tolerance, out);
292}
293
294fn flatten_cubic(p0: Vec2, p1: Vec2, p2: Vec2, p3: Vec2, tolerance: f32, out: &mut Vec<Vec2>) {
297 let d1 = perp_distance(p1, p0, p3);
298 let d2 = perp_distance(p2, p0, p3);
299 if d1.max(d2) <= tolerance {
300 out.push(p3);
301 return;
302 }
303 let q01 = (p0 + p1) * 0.5;
304 let q12 = (p1 + p2) * 0.5;
305 let q23 = (p2 + p3) * 0.5;
306 let r012 = (q01 + q12) * 0.5;
307 let r123 = (q12 + q23) * 0.5;
308 let mid = (r012 + r123) * 0.5;
309 flatten_cubic(p0, q01, r012, mid, tolerance, out);
310 flatten_cubic(mid, r123, q23, p3, tolerance, out);
311}
312
313fn perp_distance(p: Vec2, a: Vec2, b: Vec2) -> f32 {
315 let ab = b - a;
316 let len = ab.length();
317 if len < f32::EPSILON {
318 return (p - a).length();
319 }
320 let cross = (p.x - a.x) * ab.y - (p.y - a.y) * ab.x;
322 cross.abs() / len
323}
324
325#[cfg(test)]
326mod tests {
327 use super::*;
328
329 #[test]
330 fn line_only_path_produces_control_points() {
331 let path = PathBuilder::new()
332 .move_to(Vec2::new(0.0, 0.0))
333 .line_to(Vec2::new(1.0, 0.0))
334 .line_to(Vec2::new(1.0, 1.0))
335 .build();
336 let pts = path.flatten(0.01);
337 assert_eq!(pts.len(), 3);
338 assert!((pts[2].y - 1.0).abs() < f32::EPSILON);
339 }
340
341 #[test]
342 fn close_appends_back_to_start() {
343 let path = PathBuilder::new()
344 .move_to(Vec2::new(0.0, 0.0))
345 .line_to(Vec2::new(0.5, 0.0))
346 .line_to(Vec2::new(0.5, 0.5))
347 .close()
348 .build();
349 let pts = path.flatten(0.01);
350 assert_eq!(pts.len(), 4);
352 assert!((pts[3].x - 0.0).abs() < f32::EPSILON);
353 assert!((pts[3].y - 0.0).abs() < f32::EPSILON);
354 }
355
356 #[test]
357 fn quad_subdivides_when_control_off_chord() {
358 let path = PathBuilder::new()
361 .move_to(Vec2::new(0.0, 0.0))
362 .quad_to(Vec2::new(0.5, 1.0), Vec2::new(1.0, 0.0))
363 .build();
364 let pts = path.flatten(0.01);
365 assert!(
366 pts.len() > 2,
367 "off-chord quad should subdivide, got {} pts",
368 pts.len()
369 );
370 }
371
372 #[test]
373 fn cubic_with_zero_curvature_emits_two_points() {
374 let path = PathBuilder::new()
377 .move_to(Vec2::new(0.0, 0.0))
378 .cubic_to(
379 Vec2::new(0.33, 0.0),
380 Vec2::new(0.66, 0.0),
381 Vec2::new(1.0, 0.0),
382 )
383 .build();
384 let pts = path.flatten(0.01);
385 assert_eq!(pts.len(), 2, "flat cubic should not subdivide");
386 }
387
388 #[test]
389 fn perp_distance_computes_correctly() {
390 let d = perp_distance(Vec2::new(0.0, 1.0), Vec2::ZERO, Vec2::new(1.0, 0.0));
391 assert!((d - 1.0).abs() < 1e-3);
393 }
394
395 #[test]
396 fn flatten_subpaths_separates_two_movetos() {
397 let path = PathBuilder::new()
398 .move_to(Vec2::new(0.0, 0.0))
399 .line_to(Vec2::new(0.5, 0.0))
400 .line_to(Vec2::new(0.5, 0.5))
401 .close()
402 .move_to(Vec2::new(1.0, 1.0))
403 .line_to(Vec2::new(1.5, 1.0))
404 .line_to(Vec2::new(1.5, 1.5))
405 .close()
406 .build();
407 let subs = path.flatten_subpaths(0.01);
408 assert_eq!(subs.len(), 2, "two MoveTos → two subpaths");
409 assert_eq!(subs[0].len(), 4);
411 assert_eq!(subs[1].len(), 4);
412 assert!(subs[0].iter().all(|p| p.x < 0.6 && p.y < 0.6));
415 assert!(subs[1].iter().all(|p| p.x > 0.9 && p.y > 0.9));
416 }
417
418 #[test]
419 fn flatten_subpaths_preserves_bezier_curvature_per_subpath() {
420 let path = PathBuilder::new()
421 .move_to(Vec2::new(0.0, 0.0))
422 .quad_to(Vec2::new(0.5, 1.0), Vec2::new(1.0, 0.0))
423 .close()
424 .move_to(Vec2::new(2.0, 0.0))
425 .line_to(Vec2::new(3.0, 0.0))
426 .close()
427 .build();
428 let subs = path.flatten_subpaths(0.01);
429 assert_eq!(subs.len(), 2);
430 assert!(
432 subs[0].len() > 3,
433 "quad subpath should subdivide, got {} pts",
434 subs[0].len()
435 );
436 assert_eq!(subs[1].len(), 3);
439 }
440
441 #[test]
442 fn flatten_subpaths_empty_path_is_empty() {
443 let path = Path::from_commands(Vec::new());
444 assert!(path.flatten_subpaths(0.01).is_empty());
445 }
446
447 #[test]
448 fn stroke_to_graphics_emits_line_per_segment() {
449 let path = PathBuilder::new()
450 .move_to(Vec2::new(0.0, 0.0))
451 .line_to(Vec2::new(1.0, 0.0))
452 .line_to(Vec2::new(1.0, 1.0))
453 .build();
454 let g = path.stroke_to_graphics(0.02, Color::WHITE, 0.01);
455 assert_eq!(g.primitives.len(), 2);
457 }
458}