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, Debug, Default, PartialEq)]
174pub struct Path {
175 verbs: Vec<PathVerb>,
176}
177
178impl Path {
179 pub fn new() -> Self {
181 Self::default()
182 }
183
184 pub fn move_to(mut self, p: Point) -> Self {
186 self.verbs.push(PathVerb::MoveTo(p));
187 self
188 }
189
190 pub fn line_to(mut self, p: Point) -> Self {
192 self.verbs.push(PathVerb::LineTo(p));
193 self
194 }
195
196 pub fn quad_to(mut self, ctrl: Point, to: Point) -> Self {
198 self.verbs.push(PathVerb::QuadTo { ctrl, to });
199 self
200 }
201
202 pub fn cubic_to(mut self, c1: Point, c2: Point, to: Point) -> Self {
204 self.verbs.push(PathVerb::CubicTo { c1, c2, to });
205 self
206 }
207
208 pub fn close(mut self) -> Self {
210 self.verbs.push(PathVerb::Close);
211 self
212 }
213
214 #[must_use]
217 pub fn append(mut self, other: Path) -> Self {
218 self.verbs.extend(other.verbs);
219 self
220 }
221
222 pub fn verbs(&self) -> &[PathVerb] {
224 &self.verbs
225 }
226
227 pub fn is_empty(&self) -> bool {
229 self.verbs.is_empty()
230 }
231
232 pub fn rect(r: Rect) -> Self {
234 Self::new()
235 .move_to(Point::new(r.x, r.y))
236 .line_to(Point::new(r.x + r.width, r.y))
237 .line_to(Point::new(r.x + r.width, r.y + r.height))
238 .line_to(Point::new(r.x, r.y + r.height))
239 .close()
240 }
241
242 pub fn arc_to(mut self, center: Point, radius: f32, start: f32, end: f32) -> Self {
251 let sweep = end - start;
252 if sweep.abs() < 1e-6 || radius <= 0.0 {
253 return self;
254 }
255 let pieces = (sweep.abs() / std::f32::consts::FRAC_PI_2).ceil().max(1.0);
256 let step = sweep / pieces;
257 let k = 4.0 / 3.0 * (step / 4.0).tan();
260 let mut angle = start;
261 for _ in 0..pieces as usize {
262 let next = angle + step;
263 let (s0, c0) = angle.sin_cos();
264 let (s1, c1) = next.sin_cos();
265 let p0 = Point::new(center.x + radius * c0, center.y + radius * s0);
266 let p1 = Point::new(center.x + radius * c1, center.y + radius * s1);
267 self = self.cubic_to(
268 Point::new(p0.x - k * radius * s0, p0.y + k * radius * c0),
269 Point::new(p1.x + k * radius * s1, p1.y - k * radius * c1),
270 p1,
271 );
272 angle = next;
273 }
274 self
275 }
276
277 pub fn circle(center: Point, radius: f32) -> Self {
280 const K: f32 = 0.552_284_8;
281 let (cx, cy, r) = (center.x, center.y, radius);
282 let k = r * K;
283 Self::new()
284 .move_to(Point::new(cx, cy - r))
285 .cubic_to(
286 Point::new(cx + k, cy - r),
287 Point::new(cx + r, cy - k),
288 Point::new(cx + r, cy),
289 )
290 .cubic_to(
291 Point::new(cx + r, cy + k),
292 Point::new(cx + k, cy + r),
293 Point::new(cx, cy + r),
294 )
295 .cubic_to(
296 Point::new(cx - k, cy + r),
297 Point::new(cx - r, cy + k),
298 Point::new(cx - r, cy),
299 )
300 .cubic_to(
301 Point::new(cx - r, cy - k),
302 Point::new(cx - k, cy - r),
303 Point::new(cx, cy - r),
304 )
305 .close()
306 }
307
308 pub fn oval(r: Rect) -> Self {
315 const K: f32 = 0.552_284_8;
316 let (rx, ry) = (r.width * 0.5, r.height * 0.5);
317 let (cx, cy) = (r.x + rx, r.y + ry);
318 let (kx, ky) = (rx * K, ry * K);
319 Self::new()
320 .move_to(Point::new(cx, cy - ry))
321 .cubic_to(
322 Point::new(cx + kx, cy - ry),
323 Point::new(cx + rx, cy - ky),
324 Point::new(cx + rx, cy),
325 )
326 .cubic_to(
327 Point::new(cx + rx, cy + ky),
328 Point::new(cx + kx, cy + ry),
329 Point::new(cx, cy + ry),
330 )
331 .cubic_to(
332 Point::new(cx - kx, cy + ry),
333 Point::new(cx - rx, cy + ky),
334 Point::new(cx - rx, cy),
335 )
336 .cubic_to(
337 Point::new(cx - rx, cy - ky),
338 Point::new(cx - kx, cy - ry),
339 Point::new(cx, cy - ry),
340 )
341 .close()
342 }
343
344 pub fn scaled(&self, factor: f32) -> Path {
347 Path {
348 verbs: self.verbs.iter().map(|v| v.scaled(factor)).collect(),
349 }
350 }
351
352 pub fn translated(&self, dx: f32, dy: f32) -> Path {
354 Path {
355 verbs: self.verbs.iter().map(|v| v.translated(dx, dy)).collect(),
356 }
357 }
358
359 pub fn mirrored_x(&self, axis: f32) -> Path {
367 Path {
368 verbs: self.verbs.iter().map(|v| v.mirrored_x(axis)).collect(),
369 }
370 }
371
372 pub fn rotated(&self, center: Point, radians: f32) -> Path {
382 let (sin, cos) = radians.sin_cos();
383 Path {
384 verbs: self
385 .verbs
386 .iter()
387 .map(|v| v.rotated(center, sin, cos))
388 .collect(),
389 }
390 }
391}
392
393#[cfg(test)]
394mod tests {
395 use super::*;
396
397 #[test]
401 fn mirroring_reflects_x_about_the_axis_and_nothing_else() {
402 let path = Path::new()
403 .move_to(Point::new(2.0, 5.0))
404 .line_to(Point::new(8.0, 5.0))
405 .quad_to(Point::new(10.0, 7.0), Point::new(6.0, 9.0))
406 .cubic_to(
407 Point::new(4.0, 9.0),
408 Point::new(3.0, 8.0),
409 Point::new(2.0, 5.0),
410 )
411 .close();
412 let flipped = path.mirrored_x(6.0);
413 assert_eq!(
414 flipped.verbs(),
415 &[
416 PathVerb::MoveTo(Point::new(10.0, 5.0)),
417 PathVerb::LineTo(Point::new(4.0, 5.0)),
418 PathVerb::QuadTo {
419 ctrl: Point::new(2.0, 7.0),
420 to: Point::new(6.0, 9.0),
421 },
422 PathVerb::CubicTo {
423 c1: Point::new(8.0, 9.0),
424 c2: Point::new(9.0, 8.0),
425 to: Point::new(10.0, 5.0),
426 },
427 PathVerb::Close,
428 ]
429 );
430 assert_eq!(path.mirrored_x(6.0).mirrored_x(6.0).verbs(), path.verbs());
432 }
433
434 #[test]
443 fn rotating_turns_clockwise_about_the_centre() {
444 let centre = Point::new(10.0, 10.0);
445 let quarter = std::f32::consts::FRAC_PI_2;
446 let up = Path::new()
448 .move_to(centre)
449 .line_to(Point::new(10.0, 4.0))
450 .close();
451 let turned = up.rotated(centre, quarter);
452 let PathVerb::LineTo(tip) = turned.verbs()[1] else {
453 panic!("the second verb is still a line");
454 };
455 assert!(
456 (tip.x - 16.0).abs() < 1e-4 && (tip.y - 10.0).abs() < 1e-4,
457 "a quarter turn puts the tip to the right of the centre: {tip:?}"
458 );
459 let PathVerb::MoveTo(fixed) = turned.verbs()[0] else {
460 panic!("the first verb is still a move");
461 };
462 assert_eq!(fixed, centre, "the centre does not move");
463
464 let round_trip = up
466 .rotated(centre, quarter)
467 .rotated(centre, quarter)
468 .rotated(centre, quarter)
469 .rotated(centre, quarter);
470 for (a, b) in round_trip.verbs().iter().zip(up.verbs()) {
471 match (a, b) {
472 (PathVerb::MoveTo(a), PathVerb::MoveTo(b))
473 | (PathVerb::LineTo(a), PathVerb::LineTo(b)) => {
474 assert!(
475 (a.x - b.x).abs() < 1e-3 && (a.y - b.y).abs() < 1e-3,
476 "{a:?}"
477 );
478 }
479 (PathVerb::Close, PathVerb::Close) => {}
480 _ => panic!("the verbs changed kind"),
481 }
482 }
483 }
484
485 #[test]
490 fn rotating_carries_the_control_points() {
491 let centre = Point::new(0.0, 0.0);
492 let half = std::f32::consts::PI;
493 let curved = Path::new()
494 .move_to(Point::new(1.0, 0.0))
495 .quad_to(Point::new(2.0, 3.0), Point::new(4.0, 0.0))
496 .cubic_to(
497 Point::new(5.0, 1.0),
498 Point::new(6.0, 2.0),
499 Point::new(7.0, 0.0),
500 );
501 let turned = curved.rotated(centre, half);
502 let near = |a: Point, x: f32, y: f32| (a.x - x).abs() < 1e-4 && (a.y - y).abs() < 1e-4;
504 match turned.verbs()[1] {
505 PathVerb::QuadTo { ctrl, to } => {
506 assert!(near(ctrl, -2.0, -3.0), "the control point turned: {ctrl:?}");
507 assert!(near(to, -4.0, 0.0), "and so did the end: {to:?}");
508 }
509 ref other => panic!("expected a quad, got {other:?}"),
510 }
511 match turned.verbs()[2] {
512 PathVerb::CubicTo { c1, c2, to } => {
513 assert!(
514 near(c1, -5.0, -1.0) && near(c2, -6.0, -2.0),
515 "both controls"
516 );
517 assert!(near(to, -7.0, 0.0), "and the end: {to:?}");
518 }
519 ref other => panic!("expected a cubic, got {other:?}"),
520 }
521 }
522
523 #[test]
524 fn builder_records_verbs_in_order() {
525 let p = Path::new()
526 .move_to(Point::new(1.0, 2.0))
527 .line_to(Point::new(3.0, 4.0))
528 .quad_to(Point::new(5.0, 6.0), Point::new(7.0, 8.0))
529 .close();
530 assert_eq!(
531 p.verbs(),
532 &[
533 PathVerb::MoveTo(Point::new(1.0, 2.0)),
534 PathVerb::LineTo(Point::new(3.0, 4.0)),
535 PathVerb::QuadTo {
536 ctrl: Point::new(5.0, 6.0),
537 to: Point::new(7.0, 8.0)
538 },
539 PathVerb::Close,
540 ]
541 );
542 }
543
544 #[test]
547 fn an_arc_ends_on_the_circle_it_was_given() {
548 use std::f32::consts::PI;
549 let centre = Point::new(50.0, 40.0);
550 let radius = 12.0;
551 for (start, end) in [
552 (0.0, PI), (PI, 0.0), (0.3, 0.5), (-PI, PI * 0.75), ] {
557 let on = |a: f32| Point::new(centre.x + radius * a.cos(), centre.y + radius * a.sin());
558 let path = Path::new()
559 .move_to(on(start))
560 .arc_to(centre, radius, start, end);
561 let last = match path.verbs().last() {
562 Some(PathVerb::CubicTo { to, .. }) => *to,
563 other => panic!("an arc ends on a cubic, got {other:?}"),
564 };
565 let want = on(end);
566 assert!(
567 (last.x - want.x).abs() < 0.01 && (last.y - want.y).abs() < 0.01,
568 "arc {start}..{end} ended at {last:?}, wanted {want:?}"
569 );
570 for verb in path.verbs() {
572 if let PathVerb::CubicTo { to, .. } = verb {
573 let d = ((to.x - centre.x).powi(2) + (to.y - centre.y).powi(2)).sqrt();
574 assert!((d - radius).abs() < 0.01, "node off the circle: {d}");
575 }
576 }
577 }
578 }
579
580 #[test]
581 fn rect_is_a_closed_quad() {
582 let p = Path::rect(Rect::new(0.0, 0.0, 10.0, 20.0));
583 assert_eq!(p.verbs().len(), 5);
585 assert!(matches!(p.verbs().last(), Some(PathVerb::Close)));
586 }
587
588 #[test]
589 fn scaled_and_translated_transform_points() {
590 let p = Path::new()
591 .move_to(Point::new(2.0, 3.0))
592 .line_to(Point::new(4.0, 5.0));
593 let s = p.scaled(2.0);
594 assert_eq!(s.verbs()[0], PathVerb::MoveTo(Point::new(4.0, 6.0)));
595 let t = p.translated(10.0, 100.0);
596 assert_eq!(t.verbs()[1], PathVerb::LineTo(Point::new(14.0, 105.0)));
597 }
598
599 #[test]
600 fn circle_starts_at_top_and_closes() {
601 let c = Path::circle(Point::new(12.0, 12.0), 10.0);
602 assert_eq!(c.verbs()[0], PathVerb::MoveTo(Point::new(12.0, 2.0)));
603 assert!(matches!(c.verbs().last(), Some(PathVerb::Close)));
604 }
605}