1use crate::engine::renderer_model::geom::Rect;
5
6#[derive(Debug, Clone)]
8pub enum PathCommand {
9 MoveTo { x: f32, y: f32 },
11 LineTo { x: f32, y: f32 },
13 QuadTo { cx: f32, cy: f32, x: f32, y: f32 },
15 CubicTo {
18 c1x: f32,
19 c1y: f32,
20 c2x: f32,
21 c2y: f32,
22 x: f32,
23 y: f32,
24 },
25 Close,
27}
28
29#[derive(Debug, Clone)]
35pub struct Path {
36 pub commands: Vec<PathCommand>,
37 current: Option<(f32, f32)>,
38 start: Option<(f32, f32)>,
39}
40
41impl Path {
42 pub fn new() -> Self {
44 Path {
45 commands: Vec::new(),
46 current: None,
47 start: None,
48 }
49 }
50
51 pub fn move_to(&mut self, x: f32, y: f32) {
53 self.commands.push(PathCommand::MoveTo { x, y });
54 self.current = Some((x, y));
55 self.start = Some((x, y));
56 }
57
58 fn ensure_current(&mut self, x: f32, y: f32) {
61 if self.current.is_none() {
62 self.commands.push(PathCommand::MoveTo { x, y });
63 }
64 self.current = Some((x, y));
65 }
66
67 pub fn line_to(&mut self, x: f32, y: f32) {
72 self.ensure_current(x, y);
73 self.commands.push(PathCommand::LineTo { x, y });
74 }
75
76 pub fn quad_to(&mut self, c: (f32, f32), p: (f32, f32)) {
81 if self.current.is_none() {
82 self.move_to(p.0, p.1);
83 return;
84 }
85 self.commands.push(PathCommand::QuadTo {
86 cx: c.0,
87 cy: c.1,
88 x: p.0,
89 y: p.1,
90 });
91 self.current = Some((p.0, p.1));
92 }
93
94 pub fn cubic_to(&mut self, c1: (f32, f32), c2: (f32, f32), p: (f32, f32)) {
99 if self.current.is_none() {
100 self.move_to(p.0, p.1);
101 return;
102 }
103 self.commands.push(PathCommand::CubicTo {
104 c1x: c1.0,
105 c1y: c1.1,
106 c2x: c2.0,
107 c2y: c2.1,
108 x: p.0,
109 y: p.1,
110 });
111 self.current = Some((p.0, p.1));
112 }
113
114 pub fn close(&mut self) {
117 self.commands.push(PathCommand::Close);
118 self.current = self.start;
119 }
120
121 pub fn bounding_box(&self) -> Option<Rect> {
127 let mut min_x = f32::INFINITY;
128 let mut min_y = f32::INFINITY;
129 let mut max_x = f32::NEG_INFINITY;
130 let mut max_y = f32::NEG_INFINITY;
131 for cmd in &self.commands {
132 match cmd {
133 PathCommand::MoveTo { x, y } | PathCommand::LineTo { x, y } => {
134 min_x = min_x.min(*x);
135 min_y = min_y.min(*y);
136 max_x = max_x.max(*x);
137 max_y = max_y.max(*y);
138 }
139 PathCommand::QuadTo { cx, cy, x, y } => {
140 min_x = min_x.min(*x).min(*cx);
141 min_y = min_y.min(*y).min(*cy);
142 max_x = max_x.max(*x).max(*cx);
143 max_y = max_y.max(*y).max(*cy);
144 }
145 PathCommand::CubicTo {
146 c1x,
147 c1y,
148 c2x,
149 c2y,
150 x,
151 y,
152 } => {
153 min_x = min_x.min(*x).min(*c1x).min(*c2x);
154 min_y = min_y.min(*y).min(*c1y).min(*c2y);
155 max_x = max_x.max(*x).max(*c1x).max(*c2x);
156 max_y = max_y.max(*y).max(*c1y).max(*c2y);
157 }
158 PathCommand::Close => {}
159 }
160 }
161 if min_x.is_finite() && min_y.is_finite() && max_x.is_finite() && max_y.is_finite() {
162 Some(Rect::new(min_x, min_y, max_x - min_x, max_y - min_y))
163 } else {
164 None
165 }
166 }
167 pub fn subpaths(&self) -> Vec<Vec<(f32, f32)>> {
173 let mut rings: Vec<Vec<(f32, f32)>> = Vec::new();
174 let mut current: Vec<(f32, f32)> = Vec::new();
175 let mut cur_point: Option<(f32, f32)> = None;
176
177 for cmd in &self.commands {
178 match cmd {
179 PathCommand::MoveTo { x, y } => {
180 if !current.is_empty() {
181 rings.push(std::mem::take(&mut current));
182 }
183 current.push((*x, *y));
184 cur_point = Some((*x, *y));
185 }
186 PathCommand::LineTo { x, y } => {
187 current.push((*x, *y));
188 cur_point = Some((*x, *y));
189 }
190 PathCommand::QuadTo { cx, cy, x, y } => {
191 if let Some(p0) = cur_point {
192 flatten_quad(p0, (*cx, *cy), (*x, *y), &mut current);
193 } else {
194 current.push((*x, *y));
195 }
196 cur_point = Some((*x, *y));
197 }
198 PathCommand::CubicTo {
199 c1x,
200 c1y,
201 c2x,
202 c2y,
203 x,
204 y,
205 } => {
206 if let Some(p0) = cur_point {
207 flatten_cubic(p0, (*c1x, *c1y), (*c2x, *c2y), (*x, *y), &mut current);
208 } else {
209 current.push((*x, *y));
210 }
211 cur_point = Some((*x, *y));
212 }
213 PathCommand::Close => {
214 }
216 }
217 }
218 if !current.is_empty() {
219 rings.push(current);
220 }
221 rings
222 }
223
224 pub fn as_polygon_vertices(&self) -> Option<Vec<(f32, f32)>> {
230 let rings = self.subpaths();
231 let total: usize = rings.iter().map(Vec::len).sum();
232 if total < 3 {
233 return None;
234 }
235 Some(rings.into_iter().flatten().collect())
236 }
237
238 pub fn commands(&self) -> &[PathCommand] {
239 &self.commands
240 }
241}
242
243const FLATTEN_TOLERANCE: f32 = 0.25;
246
247fn flatten_cubic(
251 p0: (f32, f32),
252 c1: (f32, f32),
253 c2: (f32, f32),
254 p1: (f32, f32),
255 out: &mut Vec<(f32, f32)>,
256) {
257 let flatness = |p: (f32, f32)| -> f32 {
259 let (dx, dy) = (p1.0 - p0.0, p1.1 - p0.1);
260 let len_sq = dx * dx + dy * dy;
261 if len_sq <= f32::EPSILON {
262 ((p.0 - p0.0).powi(2) + (p.1 - p0.1).powi(2)).sqrt()
263 } else {
264 let t = (((p.0 - p0.0) * dx + (p.1 - p0.1) * dy) / len_sq).clamp(0.0, 1.0);
265 let (qx, qy) = (p0.0 + t * dx, p0.1 + t * dy);
266 ((p.0 - qx).powi(2) + (p.1 - qy).powi(2)).sqrt()
267 }
268 };
269
270 if flatness(c1) <= FLATTEN_TOLERANCE && flatness(c2) <= FLATTEN_TOLERANCE {
271 out.push(p1);
272 return;
273 }
274
275 let mid = |a: (f32, f32), b: (f32, f32)| ((a.0 + b.0) * 0.5, (a.1 + b.1) * 0.5);
277 let m01 = mid(p0, c1);
278 let m12 = mid(c1, c2);
279 let m23 = mid(c2, p1);
280 let m012 = mid(m01, m12);
281 let m123 = mid(m12, m23);
282 let m0123 = mid(m012, m123);
283
284 flatten_cubic(p0, m01, m012, m0123, out);
285 flatten_cubic(m0123, m123, m23, p1, out);
286}
287
288fn flatten_quad(p0: (f32, f32), c: (f32, f32), p1: (f32, f32), out: &mut Vec<(f32, f32)>) {
291 let c1 = (
292 p0.0 + (c.0 - p0.0) * 2.0 / 3.0,
293 p0.1 + (c.1 - p0.1) * 2.0 / 3.0,
294 );
295 let c2 = (
296 p1.0 + (c.0 - p1.0) * 2.0 / 3.0,
297 p1.1 + (c.1 - p1.1) * 2.0 / 3.0,
298 );
299 flatten_cubic(p0, c1, c2, p1, out);
300}
301
302impl Default for Path {
303 fn default() -> Self {
304 Self::new()
305 }
306}
307
308pub fn rect_path(x: f32, y: f32, w: f32, h: f32) -> Path {
311 let mut path = Path::new();
312 path.move_to(x, y);
313 path.line_to(x + w, y);
314 path.line_to(x + w, y + h);
315 path.line_to(x, y + h);
316 path.close();
317 path
318}
319
320pub fn ellipse_path(cx: f32, cy: f32, rx: f32, ry: f32) -> Path {
322 let k = 4.0 * (std::f32::consts::SQRT_2 - 1.0) / 3.0;
323 let mut path = Path::new();
324 path.move_to(cx + rx, cy);
325 path.cubic_to(
326 (cx + rx, cy - k * ry),
327 (cx + k * rx, cy - ry),
328 (cx, cy - ry),
329 );
330 path.cubic_to(
331 (cx - k * rx, cy - ry),
332 (cx - rx, cy - k * ry),
333 (cx - rx, cy),
334 );
335 path.cubic_to(
336 (cx - rx, cy + k * ry),
337 (cx - k * rx, cy + ry),
338 (cx, cy + ry),
339 );
340 path.cubic_to(
341 (cx + k * rx, cy + ry),
342 (cx + rx, cy + k * ry),
343 (cx + rx, cy),
344 );
345 path.close();
346 path
347}
348
349pub fn polygon_path(points: &[(f32, f32)]) -> Path {
351 if points.is_empty() {
352 return Path::new();
353 }
354 let mut path = Path::new();
355 path.move_to(points[0].0, points[0].1);
356 for p in &points[1..] {
357 path.line_to(p.0, p.1);
358 }
359 path.close();
360 path
361}
362
363pub fn offset_path(path: &Path, ox: f32, oy: f32) -> Path {
365 let mut out = Path::new();
366 for cmd in path.commands() {
367 match *cmd {
368 PathCommand::MoveTo { x, y } => out.move_to(x + ox, y + oy),
369 PathCommand::LineTo { x, y } => out.line_to(x + ox, y + oy),
370 PathCommand::QuadTo { cx, cy, x, y } => {
371 out.quad_to((cx + ox, cy + oy), (x + ox, y + oy))
372 }
373 PathCommand::CubicTo {
374 c1x,
375 c1y,
376 c2x,
377 c2y,
378 x,
379 y,
380 } => out.cubic_to((c1x + ox, c1y + oy), (c2x + ox, c2y + oy), (x + ox, y + oy)),
381 PathCommand::Close => out.close(),
382 }
383 }
384 out
385}
386
387pub fn clamp_radii(radii: [(f32, f32); 4], w: f32, h: f32) -> [(f32, f32); 4] {
391 let constraints = [
392 if w > 0.0 && radii[0].0 + radii[1].0 > 0.0 {
393 w / (radii[0].0 + radii[1].0)
394 } else {
395 1.0
396 },
397 if w > 0.0 && radii[2].0 + radii[3].0 > 0.0 {
398 w / (radii[2].0 + radii[3].0)
399 } else {
400 1.0
401 },
402 if h > 0.0 && radii[0].1 + radii[3].1 > 0.0 {
403 h / (radii[0].1 + radii[3].1)
404 } else {
405 1.0
406 },
407 if h > 0.0 && radii[1].1 + radii[2].1 > 0.0 {
408 h / (radii[1].1 + radii[2].1)
409 } else {
410 1.0
411 },
412 ];
413 let f = constraints.into_iter().fold(1.0f32, f32::min).max(0.0);
414 if f >= 1.0 {
415 return radii;
416 }
417 radii.map(|(rx, ry)| (rx * f, ry * f))
418}
419
420pub(crate) fn append_quarter_ellipse(
424 path: &mut Path,
425 cx: f32,
426 cy: f32,
427 rx: f32,
428 ry: f32,
429 from: (f32, f32),
430 to: (f32, f32),
431) {
432 if rx <= 0.0 || ry <= 0.0 {
433 path.line_to(to.0, to.1);
434 return;
435 }
436 let k = 4.0 * (std::f32::consts::SQRT_2 - 1.0) / 3.0;
437 let f = (from.0 - cx, from.1 - cy);
438 let t = (to.0 - cx, to.1 - cy);
439 let sign = if f.0 * t.1 - f.1 * t.0 >= 0.0 {
442 1.0
443 } else {
444 -1.0
445 };
446 let fu = (f.0 / rx, f.1 / ry);
447 let tu = (t.0 / rx, t.1 / ry);
448 let cp1 = (from.0 - sign * k * rx * fu.1, from.1 + sign * k * ry * fu.0);
449 let cp2 = (to.0 + sign * k * rx * tu.1, to.1 - sign * k * ry * tu.0);
450 path.cubic_to(cp1, cp2, to);
451}
452
453#[allow(clippy::too_many_arguments)]
458pub fn rounded_rect_path(
459 x: f32,
460 y: f32,
461 w: f32,
462 h: f32,
463 tl: (f32, f32),
464 tr: (f32, f32),
465 br: (f32, f32),
466 bl: (f32, f32),
467) -> Path {
468 let radii = clamp_radii([tl, tr, br, bl], w, h);
469 let (tl, tr, br, bl) = (radii[0], radii[1], radii[2], radii[3]);
470 let mut path = Path::new();
471 path.move_to(x + w - tr.0, y);
472 append_quarter_ellipse(
473 &mut path,
474 x + w - tr.0,
475 y + tr.1,
476 tr.0,
477 tr.1,
478 (x + w - tr.0, y),
479 (x + w, y + tr.1),
480 );
481 path.line_to(x + w, y + h - br.1);
482 append_quarter_ellipse(
483 &mut path,
484 x + w - br.0,
485 y + h - br.1,
486 br.0,
487 br.1,
488 (x + w, y + h - br.1),
489 (x + w - br.0, y + h),
490 );
491 path.line_to(x + bl.0, y + h);
492 append_quarter_ellipse(
493 &mut path,
494 x + bl.0,
495 y + h - bl.1,
496 bl.0,
497 bl.1,
498 (x + bl.0, y + h),
499 (x, y + h - bl.1),
500 );
501 path.line_to(x, y + tl.1);
502 append_quarter_ellipse(
503 &mut path,
504 x + tl.0,
505 y + tl.1,
506 tl.0,
507 tl.1,
508 (x, y + tl.1),
509 (x + tl.0, y),
510 );
511 path.close();
512 path
513}
514#[cfg(test)]
515mod tests {
516 use super::*;
517
518 fn assert_points_on_ellipse(
519 points: &[(f32, f32)],
520 cx: f32,
521 cy: f32,
522 rx: f32,
523 ry: f32,
524 tol: f32,
525 ) {
526 for (px, py) in points {
527 let v = ((px - cx) / rx).powi(2) + ((py - cy) / ry).powi(2);
528 assert!(
529 (v - 1.0).abs() < tol,
530 "point ({px},{py}) not on ellipse: {v}"
531 );
532 }
533 }
534
535 #[test]
536 fn test_rect_path_vertices_and_bounds() {
537 let path = rect_path(10.0, 20.0, 100.0, 50.0);
538 assert_eq!(
539 path.as_polygon_vertices().unwrap(),
540 vec![(10.0, 20.0), (110.0, 20.0), (110.0, 70.0), (10.0, 70.0)]
541 );
542 let bb = path.bounding_box().unwrap();
543 assert!((bb.x - 10.0).abs() < 1e-6);
544 assert!((bb.y - 20.0).abs() < 1e-6);
545 assert!((bb.width - 100.0).abs() < 1e-6);
546 assert!((bb.height - 50.0).abs() < 1e-6);
547 }
548
549 #[test]
550 fn test_polygon_path() {
551 let points = [(0.0, 0.0), (10.0, 0.0), (10.0, 10.0)];
552 let path = polygon_path(&points);
553 assert_eq!(path.as_polygon_vertices().unwrap(), points.to_vec());
554 }
555
556 #[test]
557 fn test_ellipse_path_flattens() {
558 let path = ellipse_path(0.0, 0.0, 50.0, 30.0);
559 let verts = path.as_polygon_vertices().unwrap();
560 assert!(
561 verts.len() > 8,
562 "expected a flattened ellipse, got {} vertices",
563 verts.len()
564 );
565 assert_eq!(verts.first().copied(), Some((50.0, 0.0)));
566 assert_eq!(verts.last().copied(), Some((50.0, 0.0)));
567 assert_points_on_ellipse(&verts, 0.0, 0.0, 50.0, 30.0, 0.01);
568 }
569
570 #[test]
571 fn test_rounded_rect_corners_on_ellipse() {
572 let path = rounded_rect_path(
576 100.0,
577 100.0,
578 200.0,
579 200.0,
580 (50.0, 50.0),
581 (50.0, 50.0),
582 (50.0, 50.0),
583 (50.0, 50.0),
584 );
585 let verts = path.as_polygon_vertices().unwrap();
586 let corners = [
587 ((150.0, 150.0), (-1.0, -1.0)),
588 ((250.0, 150.0), (1.0, -1.0)),
589 ((250.0, 250.0), (1.0, 1.0)),
590 ((150.0, 250.0), (-1.0, 1.0)),
591 ];
592 for (px, py) in verts {
593 for ((cx, cy), (sx, sy)) in corners {
594 if (px - cx) * sx > 0.0 && (py - cy) * sy > 0.0 {
595 let v = ((px - cx) / 50.0).powi(2) + ((py - cy) / 50.0).powi(2);
596 assert!(
597 (v - 1.0).abs() < 0.01,
598 "corner point ({px},{py}) not on radius-50 arc: {v}"
599 );
600 }
601 }
602 }
603 }
604
605 #[test]
606 fn test_quad_curve_flattens() {
607 let mut path = Path::new();
608 path.move_to(0.0, 0.0);
609 path.quad_to((10.0, 20.0), (30.0, 0.0));
610 let verts = path.as_polygon_vertices().unwrap();
611 assert_eq!(verts.first().copied(), Some((0.0, 0.0)));
612 assert_eq!(verts.last().copied(), Some((30.0, 0.0)));
613 assert!(verts.len() > 2);
614 for &(_, y) in &verts[1..verts.len() - 1] {
615 assert!(y > 0.0, "quad should bulge upward, got y={y}");
616 }
617 }
618
619 #[test]
620 fn test_cubic_curve_flattens() {
621 let mut path = Path::new();
622 path.move_to(0.0, 0.0);
623 path.cubic_to((10.0, 20.0), (20.0, 20.0), (30.0, 0.0));
624 let verts = path.as_polygon_vertices().unwrap();
625 assert_eq!(verts.first().copied(), Some((0.0, 0.0)));
626 assert_eq!(verts.last().copied(), Some((30.0, 0.0)));
627 assert!(verts.len() > 2);
628 }
629
630 #[test]
631 fn test_empty_and_degenerate_paths() {
632 assert_eq!(Path::new().as_polygon_vertices(), None);
633 let mut path = Path::new();
634 path.move_to(0.0, 0.0);
635 path.line_to(10.0, 0.0);
636 assert_eq!(path.as_polygon_vertices(), None);
637 }
638
639 #[test]
640 fn test_curve_without_current_point_moves() {
641 let mut path = Path::new();
642 path.quad_to((10.0, 10.0), (20.0, 20.0));
643 assert_eq!(path.as_polygon_vertices(), None);
644 assert_eq!(path.commands().len(), 1);
645 }
646
647 #[test]
648 fn test_curve_bounding_box_includes_controls() {
649 let mut path = Path::new();
650 path.move_to(0.0, 0.0);
651 path.quad_to((100.0, 0.0), (50.0, 50.0));
652 let bb = path.bounding_box().unwrap();
653 assert!((bb.x - 0.0).abs() < 1e-6);
654 assert!((bb.width - 100.0).abs() < 1e-6);
655 assert!((bb.y - 0.0).abs() < 1e-6);
656 assert!((bb.height - 50.0).abs() < 1e-6);
657 }
658
659 #[test]
660 fn test_subpaths_split_on_move_to() {
661 let mut path = Path::new();
662 path.move_to(0.0, 0.0);
663 path.line_to(10.0, 0.0);
664 path.line_to(10.0, 10.0);
665 path.close();
666 path.move_to(20.0, 20.0);
667 path.line_to(30.0, 20.0);
668 path.line_to(30.0, 30.0);
669 path.close();
670
671 let rings = path.subpaths();
672 assert_eq!(rings.len(), 2);
673 assert_eq!(rings[0].len(), 3);
674 assert_eq!(rings[1].len(), 3);
675 }
676}