1use crate::engine::renderer_model::geom::{AffineTransform, Rect};
5
6#[derive(Debug, Clone, PartialEq)]
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 polyline_path(points: &[(f32, f32)]) -> Path {
365 if points.is_empty() {
366 return Path::new();
367 }
368 let mut path = Path::new();
369 path.move_to(points[0].0, points[0].1);
370 for p in &points[1..] {
371 path.line_to(p.0, p.1);
372 }
373 path
374}
375
376pub fn line_path(x1: f32, y1: f32, x2: f32, y2: f32) -> Path {
378 let mut path = Path::new();
379 path.move_to(x1, y1);
380 path.line_to(x2, y2);
381 path
382}
383
384pub fn offset_path(path: &Path, ox: f32, oy: f32) -> Path {
386 let mut out = Path::new();
387 for cmd in path.commands() {
388 match *cmd {
389 PathCommand::MoveTo { x, y } => out.move_to(x + ox, y + oy),
390 PathCommand::LineTo { x, y } => out.line_to(x + ox, y + oy),
391 PathCommand::QuadTo { cx, cy, x, y } => {
392 out.quad_to((cx + ox, cy + oy), (x + ox, y + oy))
393 }
394 PathCommand::CubicTo {
395 c1x,
396 c1y,
397 c2x,
398 c2y,
399 x,
400 y,
401 } => out.cubic_to((c1x + ox, c1y + oy), (c2x + ox, c2y + oy), (x + ox, y + oy)),
402 PathCommand::Close => out.close(),
403 }
404 }
405 out
406}
407
408pub fn transform_path(path: &Path, transform: &AffineTransform) -> Path {
410 let mut out = Path::new();
411 for cmd in path.commands() {
412 match *cmd {
413 PathCommand::MoveTo { x, y } => {
414 let (x, y) = transform.apply(x, y);
415 out.move_to(x, y);
416 }
417 PathCommand::LineTo { x, y } => {
418 let (x, y) = transform.apply(x, y);
419 out.line_to(x, y);
420 }
421 PathCommand::QuadTo { cx, cy, x, y } => {
422 let (cx, cy) = transform.apply(cx, cy);
423 let (x, y) = transform.apply(x, y);
424 out.quad_to((cx, cy), (x, y));
425 }
426 PathCommand::CubicTo {
427 c1x,
428 c1y,
429 c2x,
430 c2y,
431 x,
432 y,
433 } => {
434 let (c1x, c1y) = transform.apply(c1x, c1y);
435 let (c2x, c2y) = transform.apply(c2x, c2y);
436 let (x, y) = transform.apply(x, y);
437 out.cubic_to((c1x, c1y), (c2x, c2y), (x, y));
438 }
439 PathCommand::Close => out.close(),
440 }
441 }
442 out
443}
444
445pub fn clamp_radii(radii: [(f32, f32); 4], w: f32, h: f32) -> [(f32, f32); 4] {
449 let constraints = [
450 if w > 0.0 && radii[0].0 + radii[1].0 > 0.0 {
451 w / (radii[0].0 + radii[1].0)
452 } else {
453 1.0
454 },
455 if w > 0.0 && radii[2].0 + radii[3].0 > 0.0 {
456 w / (radii[2].0 + radii[3].0)
457 } else {
458 1.0
459 },
460 if h > 0.0 && radii[0].1 + radii[3].1 > 0.0 {
461 h / (radii[0].1 + radii[3].1)
462 } else {
463 1.0
464 },
465 if h > 0.0 && radii[1].1 + radii[2].1 > 0.0 {
466 h / (radii[1].1 + radii[2].1)
467 } else {
468 1.0
469 },
470 ];
471 let f = constraints.into_iter().fold(1.0f32, f32::min).max(0.0);
472 if f >= 1.0 {
473 return radii;
474 }
475 radii.map(|(rx, ry)| (rx * f, ry * f))
476}
477
478pub(crate) fn append_quarter_ellipse(
482 path: &mut Path,
483 cx: f32,
484 cy: f32,
485 rx: f32,
486 ry: f32,
487 from: (f32, f32),
488 to: (f32, f32),
489) {
490 if rx <= 0.0 || ry <= 0.0 {
491 path.line_to(to.0, to.1);
492 return;
493 }
494 let k = 4.0 * (std::f32::consts::SQRT_2 - 1.0) / 3.0;
495 let f = (from.0 - cx, from.1 - cy);
496 let t = (to.0 - cx, to.1 - cy);
497 let sign = if f.0 * t.1 - f.1 * t.0 >= 0.0 {
500 1.0
501 } else {
502 -1.0
503 };
504 let fu = (f.0 / rx, f.1 / ry);
505 let tu = (t.0 / rx, t.1 / ry);
506 let cp1 = (from.0 - sign * k * rx * fu.1, from.1 + sign * k * ry * fu.0);
507 let cp2 = (to.0 + sign * k * rx * tu.1, to.1 - sign * k * ry * tu.0);
508 path.cubic_to(cp1, cp2, to);
509}
510
511#[allow(clippy::too_many_arguments)]
516pub fn rounded_rect_path(
517 x: f32,
518 y: f32,
519 w: f32,
520 h: f32,
521 tl: (f32, f32),
522 tr: (f32, f32),
523 br: (f32, f32),
524 bl: (f32, f32),
525) -> Path {
526 let radii = clamp_radii([tl, tr, br, bl], w, h);
527 let (tl, tr, br, bl) = (radii[0], radii[1], radii[2], radii[3]);
528 let mut path = Path::new();
529 path.move_to(x + w - tr.0, y);
530 append_quarter_ellipse(
531 &mut path,
532 x + w - tr.0,
533 y + tr.1,
534 tr.0,
535 tr.1,
536 (x + w - tr.0, y),
537 (x + w, y + tr.1),
538 );
539 path.line_to(x + w, y + h - br.1);
540 append_quarter_ellipse(
541 &mut path,
542 x + w - br.0,
543 y + h - br.1,
544 br.0,
545 br.1,
546 (x + w, y + h - br.1),
547 (x + w - br.0, y + h),
548 );
549 path.line_to(x + bl.0, y + h);
550 append_quarter_ellipse(
551 &mut path,
552 x + bl.0,
553 y + h - bl.1,
554 bl.0,
555 bl.1,
556 (x + bl.0, y + h),
557 (x, y + h - bl.1),
558 );
559 path.line_to(x, y + tl.1);
560 append_quarter_ellipse(
561 &mut path,
562 x + tl.0,
563 y + tl.1,
564 tl.0,
565 tl.1,
566 (x, y + tl.1),
567 (x + tl.0, y),
568 );
569 path.close();
570 path
571}
572
573#[derive(Debug, Clone, Copy, PartialEq)]
579pub enum StrokeCap {
580 Butt,
581 Round,
582 Square,
583}
584
585#[derive(Debug, Clone, Copy, PartialEq)]
587pub enum StrokeJoin {
588 Miter,
590 Round,
592 Bevel,
594}
595
596#[derive(Debug, Clone, PartialEq)]
603pub struct Stroke {
604 pub width: f32,
605 pub cap: StrokeCap,
606 pub join: StrokeJoin,
607 pub miter_limit: f32,
611 pub dash: Option<Vec<f32>>,
613 pub dash_offset: f32,
615}
616
617impl Stroke {
618 pub fn new(width: f32) -> Self {
619 Stroke {
620 width,
621 cap: StrokeCap::Butt,
622 join: StrokeJoin::Miter,
623 miter_limit: 4.0,
624 dash: None,
625 dash_offset: 0.0,
626 }
627 }
628}
629
630pub fn stroke_path(path: &Path, stroke: &Stroke) -> Path {
644 let mut out = Path::new();
645 let half = stroke.width * 0.5;
646 if half <= 0.0 {
647 return out;
648 }
649 for chain in stroke_chains(path) {
650 let parts = match &stroke.dash {
651 Some(pattern) => dash_chain(&chain, pattern, stroke.dash_offset),
652 None => vec![chain.points.clone()],
653 };
654 for points in parts {
655 if points.len() < 2 {
656 continue;
657 }
658 let mut pruned: Vec<(f32, f32)> = Vec::with_capacity(points.len());
666 for &p in &points {
667 push_point(&mut pruned, p);
668 }
669 if chain.closed
670 && let Some(&first) = pruned.first()
671 && let Some(&last) = pruned.last()
672 && (first.0 - last.0).abs() <= f32::EPSILON
673 && (first.1 - last.1).abs() <= f32::EPSILON
674 {
675 pruned.pop();
676 }
677 if pruned.len() < 2 {
678 continue;
679 }
680 if chain.closed {
681 stroke_closed_ring(&mut out, &pruned, stroke, half);
682 } else {
683 stroke_open_ring(&mut out, &pruned, stroke, half);
684 }
685 }
686 }
687 out
688}
689
690struct StrokeChain {
692 points: Vec<(f32, f32)>,
693 closed: bool,
694}
695
696fn stroke_chains(path: &Path) -> Vec<StrokeChain> {
700 let mut chains: Vec<StrokeChain> = Vec::new();
701 let mut cur: Option<(f32, f32)> = None;
702 for cmd in path.commands() {
703 match *cmd {
704 PathCommand::MoveTo { x, y } => {
705 chains.push(StrokeChain {
706 points: vec![(x, y)],
707 closed: false,
708 });
709 cur = Some((x, y));
710 }
711 PathCommand::LineTo { x, y } => {
712 if let Some(chain) = chains.last_mut() {
713 push_point(&mut chain.points, (x, y));
714 }
715 cur = Some((x, y));
716 }
717 PathCommand::QuadTo { cx, cy, x, y } => {
718 if let Some(chain) = chains.last_mut() {
719 if let Some(p0) = cur {
720 let mut flat = Vec::new();
721 flatten_quad(p0, (cx, cy), (x, y), &mut flat);
722 for p in flat {
723 push_point(&mut chain.points, p);
724 }
725 } else {
726 push_point(&mut chain.points, (x, y));
727 }
728 }
729 cur = Some((x, y));
730 }
731 PathCommand::CubicTo {
732 c1x,
733 c1y,
734 c2x,
735 c2y,
736 x,
737 y,
738 } => {
739 if let Some(chain) = chains.last_mut() {
740 if let Some(p0) = cur {
741 let mut flat = Vec::new();
742 flatten_cubic(p0, (c1x, c1y), (c2x, c2y), (x, y), &mut flat);
743 for p in flat {
744 push_point(&mut chain.points, p);
745 }
746 } else {
747 push_point(&mut chain.points, (x, y));
748 }
749 }
750 cur = Some((x, y));
751 }
752 PathCommand::Close => {
753 if let Some(chain) = chains.last_mut() {
754 chain.closed = true;
755 }
756 cur = None;
757 }
758 }
759 }
760 chains.retain(|chain| chain.points.len() >= 2);
761 chains
762}
763
764fn push_point(points: &mut Vec<(f32, f32)>, p: (f32, f32)) {
766 if let Some(&last) = points.last()
767 && (last.0 - p.0).abs() <= f32::EPSILON
768 && (last.1 - p.1).abs() <= f32::EPSILON
769 {
770 return;
771 }
772 points.push(p);
773}
774
775fn normal(t: (f32, f32)) -> (f32, f32) {
777 (-t.1, t.0)
778}
779
780fn tangent(a: (f32, f32), b: (f32, f32)) -> Option<(f32, f32)> {
782 let dx = b.0 - a.0;
783 let dy = b.1 - a.1;
784 let len = (dx * dx + dy * dy).sqrt();
785 (len > f32::EPSILON).then_some((dx / len, dy / len))
786}
787
788fn cross(u: (f32, f32), v: (f32, f32)) -> f32 {
789 u.0 * v.1 - u.1 * v.0
790}
791
792fn stroke_open_ring(out: &mut Path, points: &[(f32, f32)], stroke: &Stroke, w: f32) {
795 let last = points.len() - 1;
796 let Some(t_first) = tangent(points[0], points[1]) else {
797 return;
798 };
799 let Some(t_last) = tangent(points[last - 1], points[last]) else {
800 return;
801 };
802 let n_first = normal(t_first);
803
804 out.move_to(points[0].0 - w * n_first.0, points[0].1 - w * n_first.1);
805 cap_at(out, stroke.cap, w, points[0], t_first, true);
806 if last == 1 {
807 let n_last = normal(t_last);
810 out.line_to(points[1].0 + w * n_last.0, points[1].1 + w * n_last.1);
811 } else {
812 for i in 1..last {
813 let u = tangent(points[i - 1], points[i]).expect("non-degenerate chain");
814 let v = tangent(points[i], points[i + 1]).expect("non-degenerate chain");
815 join_at(out, stroke, w, points[i], u, v);
816 }
817 }
818 cap_at(out, stroke.cap, w, points[last], t_last, false);
819 for i in (1..last).rev() {
820 let u = tangent(points[i], points[i + 1]).expect("non-degenerate chain");
821 let v = tangent(points[i - 1], points[i]).expect("non-degenerate chain");
822 join_at(out, stroke, -w, points[i], u, v);
823 }
824 out.close();
825}
826
827fn stroke_closed_ring(out: &mut Path, points: &[(f32, f32)], stroke: &Stroke, w: f32) {
830 let n = points.len();
831 if n < 3 {
832 return;
833 }
834 let tangents: Vec<(f32, f32)> = (0..n)
835 .map(|i| tangent(points[i], points[(i + 1) % n]).expect("non-degenerate chain"))
836 .collect();
837
838 out.move_to(
841 points[0].0 + w * normal(tangents[0]).0,
842 points[0].1 + w * normal(tangents[0]).1,
843 );
844 for i in 1..n {
845 join_at(out, stroke, w, points[i], tangents[i - 1], tangents[i]);
846 }
847 join_at(out, stroke, w, points[0], tangents[n - 1], tangents[0]);
848 out.close();
849 out.move_to(
852 points[0].0 - w * normal(tangents[n - 1]).0,
853 points[0].1 - w * normal(tangents[n - 1]).1,
854 );
855 for i in (1..n).rev() {
856 join_at(out, stroke, -w, points[i], tangents[i], tangents[i - 1]);
857 }
858 join_at(out, stroke, -w, points[0], tangents[0], tangents[n - 1]);
859 out.close();
860}
861
862fn join_at(out: &mut Path, stroke: &Stroke, dir: f32, p: (f32, f32), u: (f32, f32), v: (f32, f32)) {
866 let to = (p.0 + dir * normal(v).0, p.1 + dir * normal(v).1);
867 match stroke.join {
868 StrokeJoin::Bevel => out.line_to(to.0, to.1),
869 StrokeJoin::Round => {
870 let from = (p.0 + dir * normal(u).0, p.1 + dir * normal(u).1);
871 append_arc(out, p, dir.abs(), from, to);
872 }
873 StrokeJoin::Miter => {
874 match miter_point(dir, p, u, v) {
877 Some(apex) => {
878 let miter_len =
879 ((apex.0 - p.0).powi(2) + (apex.1 - p.1).powi(2)).sqrt();
880 if miter_len <= stroke.miter_limit.max(1.0) * stroke.width {
881 out.line_to(apex.0, apex.1);
882 out.line_to(to.0, to.1);
883 } else {
884 out.line_to(to.0, to.1);
885 }
886 }
887 None => out.line_to(to.0, to.1),
888 }
889 }
890 }
891}
892
893fn miter_point(dir: f32, p: (f32, f32), u: (f32, f32), v: (f32, f32)) -> Option<(f32, f32)> {
896 let n_u = normal(u);
897 let n_v = normal(v);
898 let q = (p.0 + dir * n_u.0, p.1 + dir * n_u.1);
899 let r = (p.0 + dir * n_v.0, p.1 + dir * n_v.1);
900 let den = cross(u, v);
901 if den.abs() < 1e-6 {
902 return None;
903 }
904 let b = (r.0 - q.0, r.1 - q.1);
906 let lambda = cross(b, v) / den;
907 Some((q.0 + lambda * u.0, q.1 + lambda * u.1))
908}
909
910fn cap_at(out: &mut Path, cap: StrokeCap, w: f32, p: (f32, f32), t: (f32, f32), forward: bool) {
913 let n = normal(t);
914 let (from, to) = if forward {
915 (
916 (p.0 - w * n.0, p.1 - w * n.1),
917 (p.0 + w * n.0, p.1 + w * n.1),
918 )
919 } else {
920 (
921 (p.0 + w * n.0, p.1 + w * n.1),
922 (p.0 - w * n.0, p.1 - w * n.1),
923 )
924 };
925 match cap {
926 StrokeCap::Butt => out.line_to(to.0, to.1),
927 StrokeCap::Round => {
928 let out_dir = if forward { (-t.0, -t.1) } else { t };
932 let mid = (p.0 + out_dir.0 * w, p.1 + out_dir.1 * w);
933 append_arc_through(out, p, w, from, to, Some(mid));
934 }
935 StrokeCap::Square => {
936 let dir = if forward { (-t.0, -t.1) } else { t };
939 let a = (from.0 + dir.0 * w, from.1 + dir.1 * w);
940 out.line_to(a.0, a.1);
941 let b = (to.0 + dir.0 * w, to.1 + dir.1 * w);
942 out.line_to(b.0, b.1);
943 out.line_to(to.0, to.1);
944 }
945 }
946}
947
948fn arc_angle_of(center: (f32, f32), p: (f32, f32)) -> f32 {
950 (p.1 - center.1).atan2(p.0 - center.0)
951}
952
953fn normalize_delta(d: f32) -> f32 {
955 let mut d = d % std::f32::consts::TAU;
956 if d > std::f32::consts::PI {
957 d -= std::f32::consts::TAU;
958 } else if d <= -std::f32::consts::PI {
959 d += std::f32::consts::TAU;
960 }
961 d
962}
963
964fn append_arc(out: &mut Path, center: (f32, f32), r: f32, from: (f32, f32), to: (f32, f32)) {
972 append_arc_through(out, center, r, from, to, None);
973}
974
975fn append_arc_through(
976 out: &mut Path,
977 center: (f32, f32),
978 r: f32,
979 from: (f32, f32),
980 to: (f32, f32),
981 mid: Option<(f32, f32)>,
982) {
983 if r <= 0.0 {
984 out.line_to(to.0, to.1);
985 return;
986 }
987 let a0 = arc_angle_of(center, from);
988 let a1 = arc_angle_of(center, to);
989 let raw = normalize_delta(a1 - a0);
990 if raw.abs() < f32::EPSILON {
991 return;
992 }
993 let sweep = if (raw.abs() - std::f32::consts::PI).abs() < 1e-3 {
996 match mid {
997 Some(mid) => {
998 let am = arc_angle_of(center, mid);
999 if normalize_delta(am - a0).signum() > 0.0 {
1000 raw.abs()
1001 } else {
1002 -raw.abs()
1003 }
1004 }
1005 None => raw.signum() * std::f32::consts::PI,
1006 }
1007 } else {
1008 raw
1009 };
1010 let steps = (sweep.abs() / std::f32::consts::FRAC_PI_2).ceil().max(1.0) as usize;
1011 for i in 0..steps {
1012 let a = a0 + sweep * (i as f32 / steps as f32);
1013 let b = a0 + sweep * ((i + 1) as f32 / steps as f32);
1014 let k = (4.0 / 3.0) * ((b - a) * 0.25).tan();
1015 let (sa, ca) = a.sin_cos();
1016 let (sb, cb) = b.sin_cos();
1017 let (p1x, p1y) = (center.0 + r * (ca - k * sa), center.1 + r * (sa + k * ca));
1018 let (p2x, p2y) = (center.0 + r * (cb + k * sb), center.1 + r * (sb - k * cb));
1019 out.cubic_to(
1020 (p1x, p1y),
1021 (p2x, p2y),
1022 (center.0 + r * cb, center.1 + r * sb),
1023 );
1024 }
1025}
1026
1027fn dash_chain(chain: &StrokeChain, pattern: &[f32], dash_offset: f32) -> Vec<Vec<(f32, f32)>> {
1030 if pattern.is_empty() || pattern.iter().all(|d| *d <= 0.0) {
1031 return Vec::new();
1032 }
1033 let even: Vec<f32> = if pattern.len() % 2 == 1 {
1035 pattern
1036 .iter()
1037 .copied()
1038 .chain(pattern.iter().copied())
1039 .collect()
1040 } else {
1041 pattern.to_vec()
1042 };
1043 let period: f32 = even.iter().sum();
1044
1045 let mut segments: Vec<Vec<(f32, f32)>> = Vec::new();
1046 let mut current: Option<Vec<(f32, f32)>> = None;
1047 let mut t = dash_offset.rem_euclid(period);
1048 let n = chain.points.len();
1049 let last = if chain.closed { n } else { n - 1 };
1050 for i in 0..last {
1051 let a = chain.points[i];
1052 let b = chain.points[(i + 1) % n];
1053 let seg_len = ((b.0 - a.0).powi(2) + (b.1 - a.1).powi(2)).sqrt();
1054 if seg_len <= f32::EPSILON {
1055 continue;
1056 }
1057 let mut covered = 0.0;
1058 while covered < seg_len {
1059 let t_norm = t.rem_euclid(period);
1060 let phase_end = dash_phase_end(t_norm, &even);
1061 let step = (phase_end - t_norm).min(seg_len - covered);
1062 let local = (covered + step) / seg_len;
1063 let point = (a.0 + (b.0 - a.0) * local, a.1 + (b.1 - a.1) * local);
1064 if dash_is_on(t_norm, &even) {
1065 match &mut current {
1066 Some(seg) => seg.push(point),
1067 None => {
1068 let start_local = covered / seg_len;
1071 let start_point = (
1072 a.0 + (b.0 - a.0) * start_local,
1073 a.1 + (b.1 - a.1) * start_local,
1074 );
1075 let mut seg = vec![start_point];
1076 if start_point != point {
1077 seg.push(point);
1078 }
1079 current = Some(seg);
1080 }
1081 }
1082 } else if let Some(seg) = current.take() {
1083 segments.push(seg);
1084 }
1085 t = t_norm + step;
1086 covered += step;
1087 }
1088 }
1089 if let Some(seg) = current.take() {
1090 segments.push(seg);
1091 }
1092 segments
1093}
1094
1095fn dash_phase_end(t: f32, even: &[f32]) -> f32 {
1097 let mut acc = 0.0;
1098 for len in even {
1099 acc += len;
1100 if t < acc {
1101 return acc;
1102 }
1103 }
1104 acc
1105}
1106
1107fn dash_is_on(t: f32, even: &[f32]) -> bool {
1109 let mut acc = 0.0;
1110 for (idx, len) in even.iter().enumerate() {
1111 if t < acc + len {
1112 return idx % 2 == 0;
1113 }
1114 acc += len;
1115 }
1116 false
1117}
1118
1119#[cfg(test)]
1120mod tests {
1121 use super::*;
1122
1123 fn assert_points_on_ellipse(
1124 points: &[(f32, f32)],
1125 cx: f32,
1126 cy: f32,
1127 rx: f32,
1128 ry: f32,
1129 tol: f32,
1130 ) {
1131 for (px, py) in points {
1132 let v = ((px - cx) / rx).powi(2) + ((py - cy) / ry).powi(2);
1133 assert!(
1134 (v - 1.0).abs() < tol,
1135 "point ({px},{py}) not on ellipse: {v}"
1136 );
1137 }
1138 }
1139
1140 #[test]
1141 fn test_rect_path_vertices_and_bounds() {
1142 let path = rect_path(10.0, 20.0, 100.0, 50.0);
1143 assert_eq!(
1144 path.as_polygon_vertices().unwrap(),
1145 vec![(10.0, 20.0), (110.0, 20.0), (110.0, 70.0), (10.0, 70.0)]
1146 );
1147 let bb = path.bounding_box().unwrap();
1148 assert!((bb.x - 10.0).abs() < 1e-6);
1149 assert!((bb.y - 20.0).abs() < 1e-6);
1150 assert!((bb.width - 100.0).abs() < 1e-6);
1151 assert!((bb.height - 50.0).abs() < 1e-6);
1152 }
1153
1154 #[test]
1155 fn test_polygon_path() {
1156 let points = [(0.0, 0.0), (10.0, 0.0), (10.0, 10.0)];
1157 let path = polygon_path(&points);
1158 assert_eq!(path.as_polygon_vertices().unwrap(), points.to_vec());
1159 }
1160
1161 #[test]
1162 fn test_ellipse_path_flattens() {
1163 let path = ellipse_path(0.0, 0.0, 50.0, 30.0);
1164 let verts = path.as_polygon_vertices().unwrap();
1165 assert!(
1166 verts.len() > 8,
1167 "expected a flattened ellipse, got {} vertices",
1168 verts.len()
1169 );
1170 assert_eq!(verts.first().copied(), Some((50.0, 0.0)));
1171 assert_eq!(verts.last().copied(), Some((50.0, 0.0)));
1172 assert_points_on_ellipse(&verts, 0.0, 0.0, 50.0, 30.0, 0.01);
1173 }
1174
1175 #[test]
1176 fn test_rounded_rect_corners_on_ellipse() {
1177 let path = rounded_rect_path(
1181 100.0,
1182 100.0,
1183 200.0,
1184 200.0,
1185 (50.0, 50.0),
1186 (50.0, 50.0),
1187 (50.0, 50.0),
1188 (50.0, 50.0),
1189 );
1190 let verts = path.as_polygon_vertices().unwrap();
1191 let corners = [
1192 ((150.0, 150.0), (-1.0, -1.0)),
1193 ((250.0, 150.0), (1.0, -1.0)),
1194 ((250.0, 250.0), (1.0, 1.0)),
1195 ((150.0, 250.0), (-1.0, 1.0)),
1196 ];
1197 for (px, py) in verts {
1198 for ((cx, cy), (sx, sy)) in corners {
1199 if (px - cx) * sx > 0.0 && (py - cy) * sy > 0.0 {
1200 let v = ((px - cx) / 50.0).powi(2) + ((py - cy) / 50.0).powi(2);
1201 assert!(
1202 (v - 1.0).abs() < 0.01,
1203 "corner point ({px},{py}) not on radius-50 arc: {v}"
1204 );
1205 }
1206 }
1207 }
1208 }
1209
1210 #[test]
1211 fn test_quad_curve_flattens() {
1212 let mut path = Path::new();
1213 path.move_to(0.0, 0.0);
1214 path.quad_to((10.0, 20.0), (30.0, 0.0));
1215 let verts = path.as_polygon_vertices().unwrap();
1216 assert_eq!(verts.first().copied(), Some((0.0, 0.0)));
1217 assert_eq!(verts.last().copied(), Some((30.0, 0.0)));
1218 assert!(verts.len() > 2);
1219 for &(_, y) in &verts[1..verts.len() - 1] {
1220 assert!(y > 0.0, "quad should bulge upward, got y={y}");
1221 }
1222 }
1223
1224 #[test]
1225 fn test_cubic_curve_flattens() {
1226 let mut path = Path::new();
1227 path.move_to(0.0, 0.0);
1228 path.cubic_to((10.0, 20.0), (20.0, 20.0), (30.0, 0.0));
1229 let verts = path.as_polygon_vertices().unwrap();
1230 assert_eq!(verts.first().copied(), Some((0.0, 0.0)));
1231 assert_eq!(verts.last().copied(), Some((30.0, 0.0)));
1232 assert!(verts.len() > 2);
1233 }
1234
1235 #[test]
1236 fn test_empty_and_degenerate_paths() {
1237 assert_eq!(Path::new().as_polygon_vertices(), None);
1238 let mut path = Path::new();
1239 path.move_to(0.0, 0.0);
1240 path.line_to(10.0, 0.0);
1241 assert_eq!(path.as_polygon_vertices(), None);
1242 }
1243
1244 #[test]
1245 fn test_curve_without_current_point_moves() {
1246 let mut path = Path::new();
1247 path.quad_to((10.0, 10.0), (20.0, 20.0));
1248 assert_eq!(path.as_polygon_vertices(), None);
1249 assert_eq!(path.commands().len(), 1);
1250 }
1251
1252 #[test]
1253 fn test_curve_bounding_box_includes_controls() {
1254 let mut path = Path::new();
1255 path.move_to(0.0, 0.0);
1256 path.quad_to((100.0, 0.0), (50.0, 50.0));
1257 let bb = path.bounding_box().unwrap();
1258 assert!((bb.x - 0.0).abs() < 1e-6);
1259 assert!((bb.width - 100.0).abs() < 1e-6);
1260 assert!((bb.y - 0.0).abs() < 1e-6);
1261 assert!((bb.height - 50.0).abs() < 1e-6);
1262 }
1263
1264 #[test]
1265 fn test_transform_path_applies_affine_to_all_commands() {
1266 let mut path = Path::new();
1267 path.move_to(1.0, 1.0);
1268 path.cubic_to((2.0, 2.0), (3.0, 2.0), (4.0, 1.0));
1269 path.close();
1270 let transform =
1271 AffineTransform::translate(10.0, 20.0).then(&AffineTransform::scale(2.0, 2.0));
1272 let out = transform_path(&path, &transform);
1273 assert_eq!(
1274 out.commands(),
1275 &[
1276 PathCommand::MoveTo { x: 12.0, y: 22.0 },
1277 PathCommand::CubicTo {
1278 c1x: 14.0,
1279 c1y: 24.0,
1280 c2x: 16.0,
1281 c2y: 24.0,
1282 x: 18.0,
1283 y: 22.0,
1284 },
1285 PathCommand::Close,
1286 ]
1287 );
1288 }
1289
1290 #[test]
1291 fn test_subpaths_split_on_move_to() {
1292 let mut path = Path::new();
1293 path.move_to(0.0, 0.0);
1294 path.line_to(10.0, 0.0);
1295 path.line_to(10.0, 10.0);
1296 path.close();
1297 path.move_to(20.0, 20.0);
1298 path.line_to(30.0, 20.0);
1299 path.line_to(30.0, 30.0);
1300 path.close();
1301
1302 let rings = path.subpaths();
1303 assert_eq!(rings.len(), 2);
1304 assert_eq!(rings[0].len(), 3);
1305 assert_eq!(rings[1].len(), 3);
1306 }
1307
1308 fn line(x1: f32, y1: f32, x2: f32, y2: f32) -> Path {
1309 let mut path = Path::new();
1310 path.move_to(x1, y1);
1311 path.line_to(x2, y2);
1312 path
1313 }
1314
1315 fn closed_square(x: f32, y: f32, side: f32) -> Path {
1316 let mut path = Path::new();
1317 path.move_to(x, y);
1318 path.line_to(x + side, y);
1319 path.line_to(x + side, y + side);
1320 path.line_to(x, y + side);
1321 path.close();
1322 path
1323 }
1324
1325 fn assert_point(a: (f32, f32), b: (f32, f32)) {
1326 assert!(
1327 (a.0 - b.0).abs() < 1e-3 && (a.1 - b.1).abs() < 1e-3,
1328 "expected {b:?}, got {a:?}"
1329 );
1330 }
1331
1332 fn signed_area(ring: &[(f32, f32)]) -> f32 {
1334 let mut sum = 0.0;
1335 for i in 0..ring.len() {
1336 let a = ring[i];
1337 let b = ring[(i + 1) % ring.len()];
1338 sum += a.0 * b.1 - b.0 * a.1;
1339 }
1340 sum * 0.5
1341 }
1342
1343 #[test]
1344 fn test_stroke_open_line_is_a_rectangular_band() {
1345 let stroked = stroke_path(&line(0.0, 0.0, 10.0, 0.0), &Stroke::new(2.0));
1347 let ring = &stroked.subpaths()[0];
1348 assert_eq!(ring.len(), 4);
1349 assert_point(ring[0], (0.0, -1.0));
1351 assert_point(ring[1], (0.0, 1.0));
1352 assert_point(ring[2], (10.0, 1.0));
1353 assert_point(ring[3], (10.0, -1.0));
1354 }
1355
1356 #[test]
1357 fn test_stroke_round_caps_bulge_past_the_endpoints() {
1358 let bob = Stroke {
1359 cap: StrokeCap::Round,
1360 ..Stroke::new(2.0)
1361 };
1362 let stroked = stroke_path(&line(0.0, 0.0, 10.0, 0.0), &bob);
1363 let bb = stroked.bounding_box().unwrap();
1364 assert!((bb.x - -1.0).abs() < 0.5, "round cap left extent: {}", bb.x);
1365 assert!(
1366 (bb.x + bb.width - 11.0).abs() < 0.5,
1367 "round cap right extent"
1368 );
1369 }
1370
1371 #[test]
1372 fn test_stroke_square_caps_extend_by_half_width() {
1373 let bob = Stroke {
1374 cap: StrokeCap::Square,
1375 ..Stroke::new(2.0)
1376 };
1377 let stroked = stroke_path(&line(0.0, 0.0, 10.0, 0.0), &bob);
1378 let bb = stroked.bounding_box().unwrap();
1379 assert!((bb.x - -1.0).abs() < 1e-3);
1380 assert!((bb.x + bb.width - 11.0).abs() < 1e-3);
1381 }
1382
1383 #[test]
1384 fn test_stroke_closed_square_is_a_winding_band() {
1385 let stroked = stroke_path(&closed_square(0.0, 0.0, 10.0), &Stroke::new(2.0));
1388 let rings = stroked.subpaths();
1389 assert_eq!(rings.len(), 2, "expected an inner and an outer ring");
1390 let area0 = signed_area(&rings[0]);
1391 let area1 = signed_area(&rings[1]);
1392 assert!(
1393 area0 * area1 < 0.0,
1394 "rings must wind in opposite directions, got {area0} and {area1}"
1395 );
1396 let bb = stroked.bounding_box().unwrap();
1398 assert!((bb.x - -1.0).abs() < 1e-3);
1399 assert!((bb.y - -1.0).abs() < 1e-3);
1400 assert!((bb.width - 12.0).abs() < 1e-3);
1401 assert!((bb.height - 12.0).abs() < 1e-3);
1402 }
1403
1404 #[test]
1405 fn test_stroke_miter_apex_sits_on_the_diagonal() {
1406 let corner = closed_square(0.0, 0.0, 10.0);
1410 let mitered = stroke_path(&corner, &Stroke::new(2.0));
1411 let outer = &mitered.subpaths()[1];
1412 assert!(
1413 outer.iter().any(|&(px, py)| px > 10.0 + 1e-3 && py < -1e-3),
1414 "miter apex (11,-1) missing from the outer ring: {outer:?}"
1415 );
1416 let bevel = stroke_path(
1419 &corner,
1420 &Stroke {
1421 join: StrokeJoin::Bevel,
1422 ..Stroke::new(2.0)
1423 },
1424 );
1425 for ring in bevel.subpaths() {
1426 assert!(
1427 !ring.iter().any(|&(px, py)| px > 10.0 + 1e-3 && py < -1e-3),
1428 "bevel must not contain a diagonal apex: {ring:?}"
1429 );
1430 }
1431 }
1432
1433 #[test]
1434 fn test_stroke_miter_limit_collapses_sharp_joins() {
1435 let sharp = line(0.0, 0.0, 10.0, 0.0);
1438 let mut sharp = sharp;
1439 sharp.line_to(20.0, 0.05);
1440 let mitered = stroke_path(&sharp, &Stroke::new(2.0));
1441 let bevel = stroke_path(
1442 &sharp,
1443 &Stroke {
1444 join: StrokeJoin::Bevel,
1445 ..Stroke::new(2.0)
1446 },
1447 );
1448 let mb = mitered.bounding_box().unwrap();
1449 let bv = bevel.bounding_box().unwrap();
1450 assert!(
1451 (mb.width - bv.width).abs() < 0.1,
1452 "the clamped miter must not poke beyond the bevel"
1453 );
1454 }
1455
1456 #[test]
1457 fn test_stroke_dash_pattern_splits_into_segments() {
1458 let dashed = stroke_path(
1460 &line(0.0, 0.0, 10.0, 0.0),
1461 &Stroke {
1462 dash: Some(vec![4.0, 2.0]),
1463 ..Stroke::new(2.0)
1464 },
1465 );
1466 let rings = dashed.subpaths();
1467 assert_eq!(rings.len(), 2, "expected two dash segments");
1468 let x0 = rings[0][0].0;
1469 let x1 = rings[1][0].0;
1470 let starts = [x0.min(x1), x0.max(x1)];
1471 assert!((starts[0] - 0.0).abs() < 1e-3, "first dash starts at 0");
1472 assert!((starts[1] - 6.0).abs() < 1e-3, "second dash starts at 6");
1473 }
1474
1475 #[test]
1476 fn stroke_closed_chain_returning_to_start_does_not_panic() {
1477 let mut path = Path::new();
1480 path.move_to(0.0, 0.0);
1481 path.line_to(10.0, 0.0);
1482 path.line_to(10.0, 10.0);
1483 path.line_to(0.0, 0.0);
1484 path.close();
1485 let stroked = stroke_path(&path, &Stroke::new(2.0));
1486 let ring = &stroked.subpaths()[0];
1487 assert!(
1488 ring.len() >= 3,
1489 "closed ring has vertices, got {}",
1490 ring.len()
1491 );
1492 assert!(stroked.bounding_box().is_some());
1493 }
1494
1495 #[test]
1496 fn stroke_dashed_closed_chain_does_not_panic() {
1497 let mut path = Path::new();
1502 path.move_to(0.0, 0.0);
1503 path.line_to(20.0, 0.0);
1504 path.line_to(20.0, 20.0);
1505 path.line_to(0.0, 0.0);
1506 path.close();
1507 let dashed = stroke_path(
1508 &path,
1509 &Stroke {
1510 dash: Some(vec![10.0, 5.0]),
1511 ..Stroke::new(2.0)
1512 },
1513 );
1514 assert!(dashed.bounding_box().is_some());
1515 }
1516
1517 #[test]
1518 fn test_stroke_round_join_uses_an_arc_not_a_point() {
1519 let mut tri = Path::new();
1523 tri.move_to(0.0, 0.0);
1524 tri.line_to(20.0, 0.0);
1525 tri.line_to(10.0, 20.0);
1526 tri.close();
1527 let round = stroke_path(
1528 &tri,
1529 &Stroke {
1530 join: StrokeJoin::Round,
1531 cap: StrokeCap::Round,
1532 ..Stroke::new(4.0)
1533 },
1534 );
1535 let bevel = stroke_path(
1536 &tri,
1537 &Stroke {
1538 join: StrokeJoin::Bevel,
1539 ..Stroke::new(4.0)
1540 },
1541 );
1542 assert!(
1543 round.commands().len() > bevel.commands().len(),
1544 "round joins must add arc commands"
1545 );
1546 }
1547}