1use crate::{Color, Point, Rect};
11
12#[derive(Clone, Copy, Debug, PartialEq)]
14pub enum PathVerb {
15 MoveTo(Point),
17 LineTo(Point),
19 QuadTo {
21 ctrl: Point,
23 to: Point,
25 },
26 CubicTo {
28 c1: Point,
30 c2: Point,
32 to: Point,
34 },
35 Close,
37}
38
39impl PathVerb {
40 fn scaled(self, f: f32) -> PathVerb {
41 let s = |p: Point| Point::new(p.x * f, p.y * f);
42 match self {
43 PathVerb::MoveTo(p) => PathVerb::MoveTo(s(p)),
44 PathVerb::LineTo(p) => PathVerb::LineTo(s(p)),
45 PathVerb::QuadTo { ctrl, to } => PathVerb::QuadTo {
46 ctrl: s(ctrl),
47 to: s(to),
48 },
49 PathVerb::CubicTo { c1, c2, to } => PathVerb::CubicTo {
50 c1: s(c1),
51 c2: s(c2),
52 to: s(to),
53 },
54 PathVerb::Close => PathVerb::Close,
55 }
56 }
57
58 fn rotated(self, center: Point, sin: f32, cos: f32) -> PathVerb {
59 let r = |p: Point| {
60 let (dx, dy) = (p.x - center.x, p.y - center.y);
61 Point::new(
62 center.x + dx * cos - dy * sin,
63 center.y + dx * sin + dy * cos,
64 )
65 };
66 match self {
67 PathVerb::MoveTo(p) => PathVerb::MoveTo(r(p)),
68 PathVerb::LineTo(p) => PathVerb::LineTo(r(p)),
69 PathVerb::QuadTo { ctrl, to } => PathVerb::QuadTo {
70 ctrl: r(ctrl),
71 to: r(to),
72 },
73 PathVerb::CubicTo { c1, c2, to } => PathVerb::CubicTo {
74 c1: r(c1),
75 c2: r(c2),
76 to: r(to),
77 },
78 PathVerb::Close => PathVerb::Close,
79 }
80 }
81
82 fn mirrored_x(self, axis: f32) -> PathVerb {
83 let m = |p: Point| Point::new(2.0 * axis - p.x, p.y);
84 match self {
85 PathVerb::MoveTo(p) => PathVerb::MoveTo(m(p)),
86 PathVerb::LineTo(p) => PathVerb::LineTo(m(p)),
87 PathVerb::QuadTo { ctrl, to } => PathVerb::QuadTo {
88 ctrl: m(ctrl),
89 to: m(to),
90 },
91 PathVerb::CubicTo { c1, c2, to } => PathVerb::CubicTo {
92 c1: m(c1),
93 c2: m(c2),
94 to: m(to),
95 },
96 PathVerb::Close => PathVerb::Close,
97 }
98 }
99
100 fn translated(self, dx: f32, dy: f32) -> PathVerb {
101 let t = |p: Point| Point::new(p.x + dx, p.y + dy);
102 match self {
103 PathVerb::MoveTo(p) => PathVerb::MoveTo(t(p)),
104 PathVerb::LineTo(p) => PathVerb::LineTo(t(p)),
105 PathVerb::QuadTo { ctrl, to } => PathVerb::QuadTo {
106 ctrl: t(ctrl),
107 to: t(to),
108 },
109 PathVerb::CubicTo { c1, c2, to } => PathVerb::CubicTo {
110 c1: t(c1),
111 c2: t(c2),
112 to: t(to),
113 },
114 PathVerb::Close => PathVerb::Close,
115 }
116 }
117}
118
119#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
121pub enum StrokeCap {
122 #[default]
124 Butt,
125 Square,
127 Round,
129}
130
131#[derive(Clone, Copy, Debug, PartialEq)]
134pub struct Stroke {
135 pub color: Color,
137 pub width: f32,
139 pub cap: StrokeCap,
141}
142
143impl Stroke {
144 pub const fn new(color: Color, width: f32) -> Self {
146 Self {
147 color,
148 width,
149 cap: StrokeCap::Butt,
150 }
151 }
152
153 #[must_use]
155 pub const fn with_cap(mut self, cap: StrokeCap) -> Self {
156 self.cap = cap;
157 self
158 }
159}
160
161#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
172pub enum FillRule {
173 #[default]
175 EvenOdd,
176 NonZero,
178}
179
180#[derive(Clone, Debug, Default, PartialEq)]
193pub struct Path {
194 verbs: Vec<PathVerb>,
195 fill_rule: FillRule,
196}
197
198impl Path {
199 pub fn with_fill_rule(mut self, rule: FillRule) -> Self {
201 self.fill_rule = rule;
202 self
203 }
204
205 pub fn fill_rule(&self) -> FillRule {
207 self.fill_rule
208 }
209
210 pub fn new() -> Self {
212 Self::default()
213 }
214
215 pub fn move_to(mut self, p: Point) -> Self {
217 self.verbs.push(PathVerb::MoveTo(p));
218 self
219 }
220
221 pub fn line_to(mut self, p: Point) -> Self {
223 self.verbs.push(PathVerb::LineTo(p));
224 self
225 }
226
227 pub fn quad_to(mut self, ctrl: Point, to: Point) -> Self {
229 self.verbs.push(PathVerb::QuadTo { ctrl, to });
230 self
231 }
232
233 pub fn cubic_to(mut self, c1: Point, c2: Point, to: Point) -> Self {
235 self.verbs.push(PathVerb::CubicTo { c1, c2, to });
236 self
237 }
238
239 pub fn close(mut self) -> Self {
241 self.verbs.push(PathVerb::Close);
242 self
243 }
244
245 #[must_use]
248 pub fn append(mut self, other: Path) -> Self {
249 self.verbs.extend(other.verbs);
250 self
251 }
252
253 pub fn verbs(&self) -> &[PathVerb] {
255 &self.verbs
256 }
257
258 pub fn is_empty(&self) -> bool {
260 self.verbs.is_empty()
261 }
262
263 pub fn rect(r: Rect) -> Self {
265 Self::new()
266 .move_to(Point::new(r.x, r.y))
267 .line_to(Point::new(r.x + r.width, r.y))
268 .line_to(Point::new(r.x + r.width, r.y + r.height))
269 .line_to(Point::new(r.x, r.y + r.height))
270 .close()
271 }
272
273 pub fn arc_to(mut self, center: Point, radius: f32, start: f32, end: f32) -> Self {
282 let sweep = end - start;
283 if sweep.abs() < 1e-6 || radius <= 0.0 {
284 return self;
285 }
286 let pieces = (sweep.abs() / std::f32::consts::FRAC_PI_2).ceil().max(1.0);
287 let step = sweep / pieces;
288 let k = 4.0 / 3.0 * (step / 4.0).tan();
291 let mut angle = start;
292 for _ in 0..pieces as usize {
293 let next = angle + step;
294 let (s0, c0) = angle.sin_cos();
295 let (s1, c1) = next.sin_cos();
296 let p0 = Point::new(center.x + radius * c0, center.y + radius * s0);
297 let p1 = Point::new(center.x + radius * c1, center.y + radius * s1);
298 self = self.cubic_to(
299 Point::new(p0.x - k * radius * s0, p0.y + k * radius * c0),
300 Point::new(p1.x + k * radius * s1, p1.y - k * radius * c1),
301 p1,
302 );
303 angle = next;
304 }
305 self
306 }
307
308 pub fn circle(center: Point, radius: f32) -> Self {
311 const K: f32 = 0.552_284_8;
312 let (cx, cy, r) = (center.x, center.y, radius);
313 let k = r * K;
314 Self::new()
315 .move_to(Point::new(cx, cy - r))
316 .cubic_to(
317 Point::new(cx + k, cy - r),
318 Point::new(cx + r, cy - k),
319 Point::new(cx + r, cy),
320 )
321 .cubic_to(
322 Point::new(cx + r, cy + k),
323 Point::new(cx + k, cy + r),
324 Point::new(cx, cy + r),
325 )
326 .cubic_to(
327 Point::new(cx - k, cy + r),
328 Point::new(cx - r, cy + k),
329 Point::new(cx - r, cy),
330 )
331 .cubic_to(
332 Point::new(cx - r, cy - k),
333 Point::new(cx - k, cy - r),
334 Point::new(cx, cy - r),
335 )
336 .close()
337 }
338
339 pub fn oval(r: Rect) -> Self {
346 const K: f32 = 0.552_284_8;
347 let (rx, ry) = (r.width * 0.5, r.height * 0.5);
348 let (cx, cy) = (r.x + rx, r.y + ry);
349 let (kx, ky) = (rx * K, ry * K);
350 Self::new()
351 .move_to(Point::new(cx, cy - ry))
352 .cubic_to(
353 Point::new(cx + kx, cy - ry),
354 Point::new(cx + rx, cy - ky),
355 Point::new(cx + rx, cy),
356 )
357 .cubic_to(
358 Point::new(cx + rx, cy + ky),
359 Point::new(cx + kx, cy + ry),
360 Point::new(cx, cy + ry),
361 )
362 .cubic_to(
363 Point::new(cx - kx, cy + ry),
364 Point::new(cx - rx, cy + ky),
365 Point::new(cx - rx, cy),
366 )
367 .cubic_to(
368 Point::new(cx - rx, cy - ky),
369 Point::new(cx - kx, cy - ry),
370 Point::new(cx, cy - ry),
371 )
372 .close()
373 }
374
375 pub fn scaled(&self, factor: f32) -> Path {
378 Path {
379 verbs: self.verbs.iter().map(|v| v.scaled(factor)).collect(),
380 fill_rule: self.fill_rule,
381 }
382 }
383
384 pub fn translated(&self, dx: f32, dy: f32) -> Path {
386 Path {
387 verbs: self.verbs.iter().map(|v| v.translated(dx, dy)).collect(),
388 fill_rule: self.fill_rule,
389 }
390 }
391
392 pub fn mirrored_x(&self, axis: f32) -> Path {
400 Path {
401 verbs: self.verbs.iter().map(|v| v.mirrored_x(axis)).collect(),
402 fill_rule: self.fill_rule,
403 }
404 }
405
406 pub fn rotated(&self, center: Point, radians: f32) -> Path {
416 let (sin, cos) = radians.sin_cos();
417 Path {
418 verbs: self
419 .verbs
420 .iter()
421 .map(|v| v.rotated(center, sin, cos))
422 .collect(),
423 fill_rule: self.fill_rule,
424 }
425 }
426}
427
428#[cfg(test)]
429mod tests {
430 use super::*;
431
432 #[test]
434 fn a_fill_rule_survives_every_transform() {
435 let p = Path::rect(Rect::new(0.0, 0.0, 4.0, 4.0)).with_fill_rule(FillRule::NonZero);
436 assert_eq!(
437 Path::new().fill_rule(),
438 FillRule::EvenOdd,
439 "even-odd by default"
440 );
441 assert_eq!(p.scaled(2.0).fill_rule(), FillRule::NonZero);
442 assert_eq!(p.translated(1.0, 1.0).fill_rule(), FillRule::NonZero);
443 assert_eq!(p.mirrored_x(2.0).fill_rule(), FillRule::NonZero);
444 assert_eq!(
445 p.rotated(Point::new(0.0, 0.0), 1.0).fill_rule(),
446 FillRule::NonZero
447 );
448 }
449
450 #[test]
454 fn mirroring_reflects_x_about_the_axis_and_nothing_else() {
455 let path = Path::new()
456 .move_to(Point::new(2.0, 5.0))
457 .line_to(Point::new(8.0, 5.0))
458 .quad_to(Point::new(10.0, 7.0), Point::new(6.0, 9.0))
459 .cubic_to(
460 Point::new(4.0, 9.0),
461 Point::new(3.0, 8.0),
462 Point::new(2.0, 5.0),
463 )
464 .close();
465 let flipped = path.mirrored_x(6.0);
466 assert_eq!(
467 flipped.verbs(),
468 &[
469 PathVerb::MoveTo(Point::new(10.0, 5.0)),
470 PathVerb::LineTo(Point::new(4.0, 5.0)),
471 PathVerb::QuadTo {
472 ctrl: Point::new(2.0, 7.0),
473 to: Point::new(6.0, 9.0),
474 },
475 PathVerb::CubicTo {
476 c1: Point::new(8.0, 9.0),
477 c2: Point::new(9.0, 8.0),
478 to: Point::new(10.0, 5.0),
479 },
480 PathVerb::Close,
481 ]
482 );
483 assert_eq!(path.mirrored_x(6.0).mirrored_x(6.0).verbs(), path.verbs());
485 }
486
487 #[test]
496 fn rotating_turns_clockwise_about_the_centre() {
497 let centre = Point::new(10.0, 10.0);
498 let quarter = std::f32::consts::FRAC_PI_2;
499 let up = Path::new()
501 .move_to(centre)
502 .line_to(Point::new(10.0, 4.0))
503 .close();
504 let turned = up.rotated(centre, quarter);
505 let PathVerb::LineTo(tip) = turned.verbs()[1] else {
506 panic!("the second verb is still a line");
507 };
508 assert!(
509 (tip.x - 16.0).abs() < 1e-4 && (tip.y - 10.0).abs() < 1e-4,
510 "a quarter turn puts the tip to the right of the centre: {tip:?}"
511 );
512 let PathVerb::MoveTo(fixed) = turned.verbs()[0] else {
513 panic!("the first verb is still a move");
514 };
515 assert_eq!(fixed, centre, "the centre does not move");
516
517 let round_trip = up
519 .rotated(centre, quarter)
520 .rotated(centre, quarter)
521 .rotated(centre, quarter)
522 .rotated(centre, quarter);
523 for (a, b) in round_trip.verbs().iter().zip(up.verbs()) {
524 match (a, b) {
525 (PathVerb::MoveTo(a), PathVerb::MoveTo(b))
526 | (PathVerb::LineTo(a), PathVerb::LineTo(b)) => {
527 assert!(
528 (a.x - b.x).abs() < 1e-3 && (a.y - b.y).abs() < 1e-3,
529 "{a:?}"
530 );
531 }
532 (PathVerb::Close, PathVerb::Close) => {}
533 _ => panic!("the verbs changed kind"),
534 }
535 }
536 }
537
538 #[test]
543 fn rotating_carries_the_control_points() {
544 let centre = Point::new(0.0, 0.0);
545 let half = std::f32::consts::PI;
546 let curved = Path::new()
547 .move_to(Point::new(1.0, 0.0))
548 .quad_to(Point::new(2.0, 3.0), Point::new(4.0, 0.0))
549 .cubic_to(
550 Point::new(5.0, 1.0),
551 Point::new(6.0, 2.0),
552 Point::new(7.0, 0.0),
553 );
554 let turned = curved.rotated(centre, half);
555 let near = |a: Point, x: f32, y: f32| (a.x - x).abs() < 1e-4 && (a.y - y).abs() < 1e-4;
557 match turned.verbs()[1] {
558 PathVerb::QuadTo { ctrl, to } => {
559 assert!(near(ctrl, -2.0, -3.0), "the control point turned: {ctrl:?}");
560 assert!(near(to, -4.0, 0.0), "and so did the end: {to:?}");
561 }
562 ref other => panic!("expected a quad, got {other:?}"),
563 }
564 match turned.verbs()[2] {
565 PathVerb::CubicTo { c1, c2, to } => {
566 assert!(
567 near(c1, -5.0, -1.0) && near(c2, -6.0, -2.0),
568 "both controls"
569 );
570 assert!(near(to, -7.0, 0.0), "and the end: {to:?}");
571 }
572 ref other => panic!("expected a cubic, got {other:?}"),
573 }
574 }
575
576 #[test]
577 fn builder_records_verbs_in_order() {
578 let p = Path::new()
579 .move_to(Point::new(1.0, 2.0))
580 .line_to(Point::new(3.0, 4.0))
581 .quad_to(Point::new(5.0, 6.0), Point::new(7.0, 8.0))
582 .close();
583 assert_eq!(
584 p.verbs(),
585 &[
586 PathVerb::MoveTo(Point::new(1.0, 2.0)),
587 PathVerb::LineTo(Point::new(3.0, 4.0)),
588 PathVerb::QuadTo {
589 ctrl: Point::new(5.0, 6.0),
590 to: Point::new(7.0, 8.0)
591 },
592 PathVerb::Close,
593 ]
594 );
595 }
596
597 #[test]
600 fn an_arc_ends_on_the_circle_it_was_given() {
601 use std::f32::consts::PI;
602 let centre = Point::new(50.0, 40.0);
603 let radius = 12.0;
604 for (start, end) in [
605 (0.0, PI), (PI, 0.0), (0.3, 0.5), (-PI, PI * 0.75), ] {
610 let on = |a: f32| Point::new(centre.x + radius * a.cos(), centre.y + radius * a.sin());
611 let path = Path::new()
612 .move_to(on(start))
613 .arc_to(centre, radius, start, end);
614 let last = match path.verbs().last() {
615 Some(PathVerb::CubicTo { to, .. }) => *to,
616 other => panic!("an arc ends on a cubic, got {other:?}"),
617 };
618 let want = on(end);
619 assert!(
620 (last.x - want.x).abs() < 0.01 && (last.y - want.y).abs() < 0.01,
621 "arc {start}..{end} ended at {last:?}, wanted {want:?}"
622 );
623 for verb in path.verbs() {
625 if let PathVerb::CubicTo { to, .. } = verb {
626 let d = ((to.x - centre.x).powi(2) + (to.y - centre.y).powi(2)).sqrt();
627 assert!((d - radius).abs() < 0.01, "node off the circle: {d}");
628 }
629 }
630 }
631 }
632
633 #[test]
634 fn rect_is_a_closed_quad() {
635 let p = Path::rect(Rect::new(0.0, 0.0, 10.0, 20.0));
636 assert_eq!(p.verbs().len(), 5);
638 assert!(matches!(p.verbs().last(), Some(PathVerb::Close)));
639 }
640
641 #[test]
642 fn scaled_and_translated_transform_points() {
643 let p = Path::new()
644 .move_to(Point::new(2.0, 3.0))
645 .line_to(Point::new(4.0, 5.0));
646 let s = p.scaled(2.0);
647 assert_eq!(s.verbs()[0], PathVerb::MoveTo(Point::new(4.0, 6.0)));
648 let t = p.translated(10.0, 100.0);
649 assert_eq!(t.verbs()[1], PathVerb::LineTo(Point::new(14.0, 105.0)));
650 }
651
652 #[test]
653 fn circle_starts_at_top_and_closes() {
654 let c = Path::circle(Point::new(12.0, 12.0), 10.0);
655 assert_eq!(c.verbs()[0], PathVerb::MoveTo(Point::new(12.0, 2.0)));
656 assert!(matches!(c.verbs().last(), Some(PathVerb::Close)));
657 }
658}