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, PartialEq)]
121pub struct Stroke {
122 pub color: Color,
124 pub width: f32,
126}
127
128impl Stroke {
129 pub const fn new(color: Color, width: f32) -> Self {
131 Self { color, width }
132 }
133}
134
135#[derive(Clone, Debug, Default, PartialEq)]
148pub struct Path {
149 verbs: Vec<PathVerb>,
150}
151
152impl Path {
153 pub fn new() -> Self {
155 Self::default()
156 }
157
158 pub fn move_to(mut self, p: Point) -> Self {
160 self.verbs.push(PathVerb::MoveTo(p));
161 self
162 }
163
164 pub fn line_to(mut self, p: Point) -> Self {
166 self.verbs.push(PathVerb::LineTo(p));
167 self
168 }
169
170 pub fn quad_to(mut self, ctrl: Point, to: Point) -> Self {
172 self.verbs.push(PathVerb::QuadTo { ctrl, to });
173 self
174 }
175
176 pub fn cubic_to(mut self, c1: Point, c2: Point, to: Point) -> Self {
178 self.verbs.push(PathVerb::CubicTo { c1, c2, to });
179 self
180 }
181
182 pub fn close(mut self) -> Self {
184 self.verbs.push(PathVerb::Close);
185 self
186 }
187
188 pub fn verbs(&self) -> &[PathVerb] {
190 &self.verbs
191 }
192
193 pub fn is_empty(&self) -> bool {
195 self.verbs.is_empty()
196 }
197
198 pub fn rect(r: Rect) -> Self {
200 Self::new()
201 .move_to(Point::new(r.x, r.y))
202 .line_to(Point::new(r.x + r.width, r.y))
203 .line_to(Point::new(r.x + r.width, r.y + r.height))
204 .line_to(Point::new(r.x, r.y + r.height))
205 .close()
206 }
207
208 pub fn arc_to(mut self, center: Point, radius: f32, start: f32, end: f32) -> Self {
217 let sweep = end - start;
218 if sweep.abs() < 1e-6 || radius <= 0.0 {
219 return self;
220 }
221 let pieces = (sweep.abs() / std::f32::consts::FRAC_PI_2).ceil().max(1.0);
222 let step = sweep / pieces;
223 let k = 4.0 / 3.0 * (step / 4.0).tan();
226 let mut angle = start;
227 for _ in 0..pieces as usize {
228 let next = angle + step;
229 let (s0, c0) = angle.sin_cos();
230 let (s1, c1) = next.sin_cos();
231 let p0 = Point::new(center.x + radius * c0, center.y + radius * s0);
232 let p1 = Point::new(center.x + radius * c1, center.y + radius * s1);
233 self = self.cubic_to(
234 Point::new(p0.x - k * radius * s0, p0.y + k * radius * c0),
235 Point::new(p1.x + k * radius * s1, p1.y - k * radius * c1),
236 p1,
237 );
238 angle = next;
239 }
240 self
241 }
242
243 pub fn circle(center: Point, radius: f32) -> Self {
246 const K: f32 = 0.552_284_8;
247 let (cx, cy, r) = (center.x, center.y, radius);
248 let k = r * K;
249 Self::new()
250 .move_to(Point::new(cx, cy - r))
251 .cubic_to(
252 Point::new(cx + k, cy - r),
253 Point::new(cx + r, cy - k),
254 Point::new(cx + r, cy),
255 )
256 .cubic_to(
257 Point::new(cx + r, cy + k),
258 Point::new(cx + k, cy + r),
259 Point::new(cx, cy + r),
260 )
261 .cubic_to(
262 Point::new(cx - k, cy + r),
263 Point::new(cx - r, cy + k),
264 Point::new(cx - r, cy),
265 )
266 .cubic_to(
267 Point::new(cx - r, cy - k),
268 Point::new(cx - k, cy - r),
269 Point::new(cx, cy - r),
270 )
271 .close()
272 }
273
274 pub fn oval(r: Rect) -> Self {
281 const K: f32 = 0.552_284_8;
282 let (rx, ry) = (r.width * 0.5, r.height * 0.5);
283 let (cx, cy) = (r.x + rx, r.y + ry);
284 let (kx, ky) = (rx * K, ry * K);
285 Self::new()
286 .move_to(Point::new(cx, cy - ry))
287 .cubic_to(
288 Point::new(cx + kx, cy - ry),
289 Point::new(cx + rx, cy - ky),
290 Point::new(cx + rx, cy),
291 )
292 .cubic_to(
293 Point::new(cx + rx, cy + ky),
294 Point::new(cx + kx, cy + ry),
295 Point::new(cx, cy + ry),
296 )
297 .cubic_to(
298 Point::new(cx - kx, cy + ry),
299 Point::new(cx - rx, cy + ky),
300 Point::new(cx - rx, cy),
301 )
302 .cubic_to(
303 Point::new(cx - rx, cy - ky),
304 Point::new(cx - kx, cy - ry),
305 Point::new(cx, cy - ry),
306 )
307 .close()
308 }
309
310 pub fn scaled(&self, factor: f32) -> Path {
313 Path {
314 verbs: self.verbs.iter().map(|v| v.scaled(factor)).collect(),
315 }
316 }
317
318 pub fn translated(&self, dx: f32, dy: f32) -> Path {
320 Path {
321 verbs: self.verbs.iter().map(|v| v.translated(dx, dy)).collect(),
322 }
323 }
324
325 pub fn mirrored_x(&self, axis: f32) -> Path {
333 Path {
334 verbs: self.verbs.iter().map(|v| v.mirrored_x(axis)).collect(),
335 }
336 }
337
338 pub fn rotated(&self, center: Point, radians: f32) -> Path {
348 let (sin, cos) = radians.sin_cos();
349 Path {
350 verbs: self
351 .verbs
352 .iter()
353 .map(|v| v.rotated(center, sin, cos))
354 .collect(),
355 }
356 }
357}
358
359#[cfg(test)]
360mod tests {
361 use super::*;
362
363 #[test]
367 fn mirroring_reflects_x_about_the_axis_and_nothing_else() {
368 let path = Path::new()
369 .move_to(Point::new(2.0, 5.0))
370 .line_to(Point::new(8.0, 5.0))
371 .quad_to(Point::new(10.0, 7.0), Point::new(6.0, 9.0))
372 .cubic_to(
373 Point::new(4.0, 9.0),
374 Point::new(3.0, 8.0),
375 Point::new(2.0, 5.0),
376 )
377 .close();
378 let flipped = path.mirrored_x(6.0);
379 assert_eq!(
380 flipped.verbs(),
381 &[
382 PathVerb::MoveTo(Point::new(10.0, 5.0)),
383 PathVerb::LineTo(Point::new(4.0, 5.0)),
384 PathVerb::QuadTo {
385 ctrl: Point::new(2.0, 7.0),
386 to: Point::new(6.0, 9.0),
387 },
388 PathVerb::CubicTo {
389 c1: Point::new(8.0, 9.0),
390 c2: Point::new(9.0, 8.0),
391 to: Point::new(10.0, 5.0),
392 },
393 PathVerb::Close,
394 ]
395 );
396 assert_eq!(path.mirrored_x(6.0).mirrored_x(6.0).verbs(), path.verbs());
398 }
399
400 #[test]
409 fn rotating_turns_clockwise_about_the_centre() {
410 let centre = Point::new(10.0, 10.0);
411 let quarter = std::f32::consts::FRAC_PI_2;
412 let up = Path::new()
414 .move_to(centre)
415 .line_to(Point::new(10.0, 4.0))
416 .close();
417 let turned = up.rotated(centre, quarter);
418 let PathVerb::LineTo(tip) = turned.verbs()[1] else {
419 panic!("the second verb is still a line");
420 };
421 assert!(
422 (tip.x - 16.0).abs() < 1e-4 && (tip.y - 10.0).abs() < 1e-4,
423 "a quarter turn puts the tip to the right of the centre: {tip:?}"
424 );
425 let PathVerb::MoveTo(fixed) = turned.verbs()[0] else {
426 panic!("the first verb is still a move");
427 };
428 assert_eq!(fixed, centre, "the centre does not move");
429
430 let round_trip = up
432 .rotated(centre, quarter)
433 .rotated(centre, quarter)
434 .rotated(centre, quarter)
435 .rotated(centre, quarter);
436 for (a, b) in round_trip.verbs().iter().zip(up.verbs()) {
437 match (a, b) {
438 (PathVerb::MoveTo(a), PathVerb::MoveTo(b))
439 | (PathVerb::LineTo(a), PathVerb::LineTo(b)) => {
440 assert!(
441 (a.x - b.x).abs() < 1e-3 && (a.y - b.y).abs() < 1e-3,
442 "{a:?}"
443 );
444 }
445 (PathVerb::Close, PathVerb::Close) => {}
446 _ => panic!("the verbs changed kind"),
447 }
448 }
449 }
450
451 #[test]
456 fn rotating_carries_the_control_points() {
457 let centre = Point::new(0.0, 0.0);
458 let half = std::f32::consts::PI;
459 let curved = Path::new()
460 .move_to(Point::new(1.0, 0.0))
461 .quad_to(Point::new(2.0, 3.0), Point::new(4.0, 0.0))
462 .cubic_to(
463 Point::new(5.0, 1.0),
464 Point::new(6.0, 2.0),
465 Point::new(7.0, 0.0),
466 );
467 let turned = curved.rotated(centre, half);
468 let near = |a: Point, x: f32, y: f32| (a.x - x).abs() < 1e-4 && (a.y - y).abs() < 1e-4;
470 match turned.verbs()[1] {
471 PathVerb::QuadTo { ctrl, to } => {
472 assert!(near(ctrl, -2.0, -3.0), "the control point turned: {ctrl:?}");
473 assert!(near(to, -4.0, 0.0), "and so did the end: {to:?}");
474 }
475 ref other => panic!("expected a quad, got {other:?}"),
476 }
477 match turned.verbs()[2] {
478 PathVerb::CubicTo { c1, c2, to } => {
479 assert!(
480 near(c1, -5.0, -1.0) && near(c2, -6.0, -2.0),
481 "both controls"
482 );
483 assert!(near(to, -7.0, 0.0), "and the end: {to:?}");
484 }
485 ref other => panic!("expected a cubic, got {other:?}"),
486 }
487 }
488
489 #[test]
490 fn builder_records_verbs_in_order() {
491 let p = Path::new()
492 .move_to(Point::new(1.0, 2.0))
493 .line_to(Point::new(3.0, 4.0))
494 .quad_to(Point::new(5.0, 6.0), Point::new(7.0, 8.0))
495 .close();
496 assert_eq!(
497 p.verbs(),
498 &[
499 PathVerb::MoveTo(Point::new(1.0, 2.0)),
500 PathVerb::LineTo(Point::new(3.0, 4.0)),
501 PathVerb::QuadTo {
502 ctrl: Point::new(5.0, 6.0),
503 to: Point::new(7.0, 8.0)
504 },
505 PathVerb::Close,
506 ]
507 );
508 }
509
510 #[test]
513 fn an_arc_ends_on_the_circle_it_was_given() {
514 use std::f32::consts::PI;
515 let centre = Point::new(50.0, 40.0);
516 let radius = 12.0;
517 for (start, end) in [
518 (0.0, PI), (PI, 0.0), (0.3, 0.5), (-PI, PI * 0.75), ] {
523 let on = |a: f32| Point::new(centre.x + radius * a.cos(), centre.y + radius * a.sin());
524 let path = Path::new()
525 .move_to(on(start))
526 .arc_to(centre, radius, start, end);
527 let last = match path.verbs().last() {
528 Some(PathVerb::CubicTo { to, .. }) => *to,
529 other => panic!("an arc ends on a cubic, got {other:?}"),
530 };
531 let want = on(end);
532 assert!(
533 (last.x - want.x).abs() < 0.01 && (last.y - want.y).abs() < 0.01,
534 "arc {start}..{end} ended at {last:?}, wanted {want:?}"
535 );
536 for verb in path.verbs() {
538 if let PathVerb::CubicTo { to, .. } = verb {
539 let d = ((to.x - centre.x).powi(2) + (to.y - centre.y).powi(2)).sqrt();
540 assert!((d - radius).abs() < 0.01, "node off the circle: {d}");
541 }
542 }
543 }
544 }
545
546 #[test]
547 fn rect_is_a_closed_quad() {
548 let p = Path::rect(Rect::new(0.0, 0.0, 10.0, 20.0));
549 assert_eq!(p.verbs().len(), 5);
551 assert!(matches!(p.verbs().last(), Some(PathVerb::Close)));
552 }
553
554 #[test]
555 fn scaled_and_translated_transform_points() {
556 let p = Path::new()
557 .move_to(Point::new(2.0, 3.0))
558 .line_to(Point::new(4.0, 5.0));
559 let s = p.scaled(2.0);
560 assert_eq!(s.verbs()[0], PathVerb::MoveTo(Point::new(4.0, 6.0)));
561 let t = p.translated(10.0, 100.0);
562 assert_eq!(t.verbs()[1], PathVerb::LineTo(Point::new(14.0, 105.0)));
563 }
564
565 #[test]
566 fn circle_starts_at_top_and_closes() {
567 let c = Path::circle(Point::new(12.0, 12.0), 10.0);
568 assert_eq!(c.verbs()[0], PathVerb::MoveTo(Point::new(12.0, 2.0)));
569 assert!(matches!(c.verbs().last(), Some(PathVerb::Close)));
570 }
571}