1use lyon_tessellation::{
4 geometry_builder::{simple_builder, VertexBuffers},
5 math::point as lyon_point,
6 path::Path as LyonPath,
7 FillOptions, FillRule as LyonFillRule, FillTessellator, LineCap as LyonLineCap,
8 LineJoin as LyonLineJoin, StrokeOptions, StrokeTessellator,
9};
10
11use crate::{ColorRgba, StrokeStyle, TextStyle, UiPoint, UiRect};
12
13#[derive(Debug, Clone, Copy, PartialEq)]
14pub struct PixelSnapPolicy {
15 pub scale_factor: f32,
16}
17
18impl PixelSnapPolicy {
19 pub const DISABLED: Self = Self { scale_factor: 0.0 };
20
21 pub fn new(scale_factor: f32) -> Self {
22 if scale_factor.is_finite() && scale_factor > 0.0 {
23 Self { scale_factor }
24 } else {
25 Self::DISABLED
26 }
27 }
28
29 pub const fn disabled() -> Self {
30 Self::DISABLED
31 }
32
33 pub const fn enabled(self) -> bool {
34 self.scale_factor > 0.0
35 }
36
37 pub fn pixel_size(self) -> f32 {
38 if self.enabled() {
39 1.0 / self.scale_factor
40 } else {
41 0.0
42 }
43 }
44
45 pub fn snap_value(self, value: f32) -> f32 {
46 if !self.enabled() || !value.is_finite() {
47 return value;
48 }
49 (value * self.scale_factor).round() / self.scale_factor
50 }
51
52 pub fn snap_center_value(self, value: f32) -> f32 {
53 if !self.enabled() || !value.is_finite() {
54 return value;
55 }
56 ((value * self.scale_factor).floor() + 0.5) / self.scale_factor
57 }
58
59 pub fn snap_point(self, point: UiPoint) -> UiPoint {
60 UiPoint::new(self.snap_value(point.x), self.snap_value(point.y))
61 }
62
63 pub fn snap_center_point(self, point: UiPoint) -> UiPoint {
64 UiPoint::new(
65 self.snap_center_value(point.x),
66 self.snap_center_value(point.y),
67 )
68 }
69
70 pub fn snap_rect(self, rect: UiRect) -> UiRect {
71 if !self.enabled() {
72 return rect;
73 }
74 let left = self.snap_value(rect.x);
75 let top = self.snap_value(rect.y);
76 let right = self.snap_value(rect.right());
77 let bottom = self.snap_value(rect.bottom());
78 UiRect::new(left, top, (right - left).max(0.0), (bottom - top).max(0.0))
79 }
80
81 pub fn snap_line_segment(self, from: UiPoint, to: UiPoint) -> (UiPoint, UiPoint) {
82 if (from.x - to.x).abs() <= f32::EPSILON {
83 let x = self.snap_center_value(from.x);
84 return (
85 UiPoint::new(x, self.snap_value(from.y)),
86 UiPoint::new(x, self.snap_value(to.y)),
87 );
88 }
89 if (from.y - to.y).abs() <= f32::EPSILON {
90 let y = self.snap_center_value(from.y);
91 return (
92 UiPoint::new(self.snap_value(from.x), y),
93 UiPoint::new(self.snap_value(to.x), y),
94 );
95 }
96 (self.snap_point(from), self.snap_point(to))
97 }
98
99 pub fn snap_stroke_width(self, width: f32) -> f32 {
100 if !self.enabled() || !width.is_finite() || width <= 0.0 {
101 return width;
102 }
103 ((width * self.scale_factor).ceil().max(1.0)) / self.scale_factor
104 }
105
106 pub fn snap_stroke(self, stroke: StrokeStyle) -> StrokeStyle {
107 StrokeStyle::new(stroke.color, self.snap_stroke_width(stroke.width))
108 }
109}
110
111#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
112pub enum StrokeAlignment {
113 Inside,
114 #[default]
115 Center,
116 Outside,
117}
118
119#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
120pub enum StrokeLineCap {
121 Butt,
122 Square,
123 #[default]
124 Round,
125}
126
127#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
128pub enum StrokeLineJoin {
129 Miter,
130 Bevel,
131 #[default]
132 Round,
133}
134
135#[derive(Debug, Clone, Copy, PartialEq)]
136pub struct PathStrokeOptions {
137 pub line_cap: StrokeLineCap,
138 pub line_join: StrokeLineJoin,
139 pub miter_limit: f32,
140}
141
142impl PathStrokeOptions {
143 pub const DEFAULT_MITER_LIMIT: f32 = 4.0;
144
145 pub const fn new() -> Self {
146 Self {
147 line_cap: StrokeLineCap::Round,
148 line_join: StrokeLineJoin::Round,
149 miter_limit: Self::DEFAULT_MITER_LIMIT,
150 }
151 }
152
153 pub const fn line_cap(mut self, line_cap: StrokeLineCap) -> Self {
154 self.line_cap = line_cap;
155 self
156 }
157
158 pub const fn line_join(mut self, line_join: StrokeLineJoin) -> Self {
159 self.line_join = line_join;
160 self
161 }
162
163 pub const fn miter_limit(mut self, miter_limit: f32) -> Self {
164 self.miter_limit = miter_limit;
165 self
166 }
167}
168
169impl Default for PathStrokeOptions {
170 fn default() -> Self {
171 Self::new()
172 }
173}
174
175#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
176pub enum PathFillRule {
177 NonZero,
178 #[default]
179 EvenOdd,
180}
181
182#[derive(Debug, Clone, Copy, PartialEq)]
183pub struct AlignedStroke {
184 pub style: StrokeStyle,
185 pub alignment: StrokeAlignment,
186}
187
188impl AlignedStroke {
189 pub const fn new(style: StrokeStyle, alignment: StrokeAlignment) -> Self {
190 Self { style, alignment }
191 }
192
193 pub const fn is_visible(self) -> bool {
194 self.style.is_visible()
195 }
196
197 pub const fn inside(style: StrokeStyle) -> Self {
198 Self::new(style, StrokeAlignment::Inside)
199 }
200
201 pub const fn center(style: StrokeStyle) -> Self {
202 Self::new(style, StrokeAlignment::Center)
203 }
204
205 pub const fn outside(style: StrokeStyle) -> Self {
206 Self::new(style, StrokeAlignment::Outside)
207 }
208}
209
210impl From<StrokeStyle> for AlignedStroke {
211 fn from(style: StrokeStyle) -> Self {
212 Self::center(style)
213 }
214}
215
216#[derive(Debug, Clone, Copy, PartialEq)]
217pub struct GradientStop {
218 pub offset: f32,
219 pub color: ColorRgba,
220}
221
222impl GradientStop {
223 pub fn new(offset: f32, color: ColorRgba) -> Self {
224 Self {
225 offset: offset.clamp(0.0, 1.0),
226 color,
227 }
228 }
229}
230
231#[derive(Debug, Clone, PartialEq)]
232pub struct LinearGradient {
233 pub start: UiPoint,
234 pub end: UiPoint,
235 pub stops: Vec<GradientStop>,
236 pub fallback: ColorRgba,
237}
238
239impl LinearGradient {
240 pub fn new(start: UiPoint, end: UiPoint, from: ColorRgba, to: ColorRgba) -> Self {
241 Self {
242 start,
243 end,
244 stops: vec![GradientStop::new(0.0, from), GradientStop::new(1.0, to)],
245 fallback: from,
246 }
247 }
248
249 pub fn stop(mut self, offset: f32, color: ColorRgba) -> Self {
250 self.stops.push(GradientStop::new(offset, color));
251 self.stops.sort_by(|a, b| a.offset.total_cmp(&b.offset));
252 self
253 }
254
255 pub const fn fallback(mut self, fallback: ColorRgba) -> Self {
256 self.fallback = fallback;
257 self
258 }
259
260 pub fn translated(mut self, offset: UiPoint) -> Self {
261 self.start.x += offset.x;
262 self.start.y += offset.y;
263 self.end.x += offset.x;
264 self.end.y += offset.y;
265 self
266 }
267}
268
269#[derive(Debug, Clone, PartialEq)]
270pub enum PaintBrush {
271 Solid(ColorRgba),
272 LinearGradient(LinearGradient),
273}
274
275impl PaintBrush {
276 pub const fn solid(color: ColorRgba) -> Self {
277 Self::Solid(color)
278 }
279
280 pub fn linear_gradient(start: UiPoint, end: UiPoint, from: ColorRgba, to: ColorRgba) -> Self {
281 Self::LinearGradient(LinearGradient::new(start, end, from, to))
282 }
283
284 pub const fn fallback_color(&self) -> ColorRgba {
285 match self {
286 Self::Solid(color) => *color,
287 Self::LinearGradient(gradient) => gradient.fallback,
288 }
289 }
290
291 pub const fn is_visible(&self) -> bool {
292 self.fallback_color().a > 0
293 }
294
295 pub fn translated(&self, offset: UiPoint) -> Self {
296 match self {
297 Self::Solid(color) => Self::Solid(*color),
298 Self::LinearGradient(gradient) => {
299 Self::LinearGradient(gradient.clone().translated(offset))
300 }
301 }
302 }
303}
304
305impl From<ColorRgba> for PaintBrush {
306 fn from(color: ColorRgba) -> Self {
307 Self::Solid(color)
308 }
309}
310
311#[derive(Debug, Clone, Copy, PartialEq)]
312pub struct CornerRadii {
313 pub top_left: f32,
314 pub top_right: f32,
315 pub bottom_right: f32,
316 pub bottom_left: f32,
317}
318
319impl CornerRadii {
320 pub const ZERO: Self = Self::uniform(0.0);
321
322 pub const fn uniform(radius: f32) -> Self {
323 Self {
324 top_left: radius,
325 top_right: radius,
326 bottom_right: radius,
327 bottom_left: radius,
328 }
329 }
330
331 pub const fn new(top_left: f32, top_right: f32, bottom_right: f32, bottom_left: f32) -> Self {
332 Self {
333 top_left,
334 top_right,
335 bottom_right,
336 bottom_left,
337 }
338 }
339
340 pub fn max_radius(self) -> f32 {
341 self.top_left
342 .max(self.top_right)
343 .max(self.bottom_right)
344 .max(self.bottom_left)
345 }
346}
347
348impl Default for CornerRadii {
349 fn default() -> Self {
350 Self::ZERO
351 }
352}
353
354#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
355pub enum PaintEffectKind {
356 Shadow,
357 Glow,
358 InsetShadow,
359}
360
361#[derive(Debug, Clone, Copy, PartialEq)]
362pub struct PaintEffect {
363 pub kind: PaintEffectKind,
364 pub color: ColorRgba,
365 pub offset: UiPoint,
366 pub blur_radius: f32,
367 pub spread: f32,
368}
369
370impl PaintEffect {
371 pub const fn shadow(color: ColorRgba, offset: UiPoint, blur_radius: f32, spread: f32) -> Self {
372 Self {
373 kind: PaintEffectKind::Shadow,
374 color,
375 offset,
376 blur_radius,
377 spread,
378 }
379 }
380
381 pub const fn glow(color: ColorRgba, blur_radius: f32, spread: f32) -> Self {
382 Self {
383 kind: PaintEffectKind::Glow,
384 color,
385 offset: UiPoint::new(0.0, 0.0),
386 blur_radius,
387 spread,
388 }
389 }
390
391 pub const fn inset_shadow(
392 color: ColorRgba,
393 offset: UiPoint,
394 blur_radius: f32,
395 spread: f32,
396 ) -> Self {
397 Self {
398 kind: PaintEffectKind::InsetShadow,
399 color,
400 offset,
401 blur_radius,
402 spread,
403 }
404 }
405}
406
407#[derive(Debug, Clone, PartialEq)]
408pub struct PaintRect {
409 pub rect: UiRect,
410 pub fill: PaintBrush,
411 pub stroke: Option<AlignedStroke>,
412 pub corner_radii: CornerRadii,
413 pub effects: Vec<PaintEffect>,
414}
415
416impl PaintRect {
417 pub fn new(rect: UiRect, fill: impl Into<PaintBrush>) -> Self {
418 Self {
419 rect,
420 fill: fill.into(),
421 stroke: None,
422 corner_radii: CornerRadii::ZERO,
423 effects: Vec::new(),
424 }
425 }
426
427 pub fn solid(rect: UiRect, fill: ColorRgba) -> Self {
428 Self::new(rect, fill)
429 }
430
431 pub fn stroke(mut self, stroke: impl Into<AlignedStroke>) -> Self {
432 let stroke = stroke.into();
433 self.stroke = stroke.is_visible().then_some(stroke);
434 self
435 }
436
437 pub const fn corner_radii(mut self, corner_radii: CornerRadii) -> Self {
438 self.corner_radii = corner_radii;
439 self
440 }
441
442 pub fn effect(mut self, effect: PaintEffect) -> Self {
443 self.effects.push(effect);
444 self
445 }
446
447 pub fn translated(mut self, offset: UiPoint) -> Self {
448 self.rect.x += offset.x;
449 self.rect.y += offset.y;
450 self.fill = self.fill.translated(offset);
451 self
452 }
453
454 pub fn pixel_snapped(mut self, policy: PixelSnapPolicy) -> Self {
455 self.rect = policy.snap_rect(self.rect);
456 if let Some(stroke) = self.stroke {
457 self.stroke = Some(AlignedStroke {
458 style: policy.snap_stroke(stroke.style),
459 alignment: stroke.alignment,
460 });
461 }
462 self
463 }
464}
465
466#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
467pub enum TextHorizontalAlign {
468 #[default]
469 Start,
470 Center,
471 End,
472}
473
474#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
475pub enum TextVerticalAlign {
476 #[default]
477 Top,
478 Center,
479 Baseline,
480 Bottom,
481}
482
483#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
484pub enum TextOverflow {
485 #[default]
486 Clip,
487 Ellipsis,
488}
489
490#[derive(Debug, Clone, PartialEq)]
491pub struct PaintText {
492 pub text: String,
493 pub rect: UiRect,
494 pub style: TextStyle,
495 pub horizontal_align: TextHorizontalAlign,
496 pub vertical_align: TextVerticalAlign,
497 pub overflow: TextOverflow,
498 pub multiline: bool,
499}
500
501impl PaintText {
502 pub fn new(text: impl Into<String>, rect: UiRect, style: TextStyle) -> Self {
503 Self {
504 text: text.into(),
505 rect,
506 style,
507 horizontal_align: TextHorizontalAlign::Start,
508 vertical_align: TextVerticalAlign::Top,
509 overflow: TextOverflow::Clip,
510 multiline: true,
511 }
512 }
513
514 pub const fn horizontal_align(mut self, align: TextHorizontalAlign) -> Self {
515 self.horizontal_align = align;
516 self
517 }
518
519 pub const fn vertical_align(mut self, align: TextVerticalAlign) -> Self {
520 self.vertical_align = align;
521 self
522 }
523
524 pub const fn overflow(mut self, overflow: TextOverflow) -> Self {
525 self.overflow = overflow;
526 self
527 }
528
529 pub const fn multiline(mut self, multiline: bool) -> Self {
530 self.multiline = multiline;
531 self
532 }
533
534 pub fn translated(mut self, offset: UiPoint) -> Self {
535 self.rect.x += offset.x;
536 self.rect.y += offset.y;
537 self
538 }
539}
540
541#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
542pub enum ImageFit {
543 #[default]
544 Fill,
545 Contain,
546 Cover,
547 Original,
548}
549
550#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
551pub enum ImageAlignment {
552 #[default]
553 Center,
554 Start,
555 End,
556}
557
558#[derive(Debug, Clone, PartialEq)]
559pub struct PaintImage {
560 pub key: String,
561 pub rect: UiRect,
562 pub tint: Option<ColorRgba>,
563 pub fit: ImageFit,
564 pub horizontal_align: ImageAlignment,
565 pub vertical_align: ImageAlignment,
566}
567
568impl PaintImage {
569 pub fn new(key: impl Into<String>, rect: UiRect) -> Self {
570 Self {
571 key: key.into(),
572 rect,
573 tint: None,
574 fit: ImageFit::Fill,
575 horizontal_align: ImageAlignment::Center,
576 vertical_align: ImageAlignment::Center,
577 }
578 }
579
580 pub const fn tinted(mut self, tint: ColorRgba) -> Self {
581 self.tint = Some(tint);
582 self
583 }
584
585 pub const fn fit(mut self, fit: ImageFit) -> Self {
586 self.fit = fit;
587 self
588 }
589
590 pub const fn align(mut self, horizontal: ImageAlignment, vertical: ImageAlignment) -> Self {
591 self.horizontal_align = horizontal;
592 self.vertical_align = vertical;
593 self
594 }
595
596 pub fn translated(mut self, offset: UiPoint) -> Self {
597 self.rect.x += offset.x;
598 self.rect.y += offset.y;
599 self
600 }
601}
602
603#[derive(Debug, Clone, Copy, PartialEq)]
604pub enum PathVerb {
605 MoveTo(UiPoint),
606 LineTo(UiPoint),
607 QuadraticTo {
608 control: UiPoint,
609 to: UiPoint,
610 },
611 CubicTo {
612 control_a: UiPoint,
613 control_b: UiPoint,
614 to: UiPoint,
615 },
616 Close,
617}
618
619impl PathVerb {
620 pub fn translated(self, offset: UiPoint) -> Self {
621 match self {
622 Self::MoveTo(point) => Self::MoveTo(translated_point(point, offset)),
623 Self::LineTo(point) => Self::LineTo(translated_point(point, offset)),
624 Self::QuadraticTo { control, to } => Self::QuadraticTo {
625 control: translated_point(control, offset),
626 to: translated_point(to, offset),
627 },
628 Self::CubicTo {
629 control_a,
630 control_b,
631 to,
632 } => Self::CubicTo {
633 control_a: translated_point(control_a, offset),
634 control_b: translated_point(control_b, offset),
635 to: translated_point(to, offset),
636 },
637 Self::Close => Self::Close,
638 }
639 }
640
641 pub fn pixel_snapped(self, policy: PixelSnapPolicy) -> Self {
642 match self {
643 Self::MoveTo(point) => Self::MoveTo(policy.snap_point(point)),
644 Self::LineTo(point) => Self::LineTo(policy.snap_point(point)),
645 Self::QuadraticTo { control, to } => Self::QuadraticTo {
646 control: policy.snap_point(control),
647 to: policy.snap_point(to),
648 },
649 Self::CubicTo {
650 control_a,
651 control_b,
652 to,
653 } => Self::CubicTo {
654 control_a: policy.snap_point(control_a),
655 control_b: policy.snap_point(control_b),
656 to: policy.snap_point(to),
657 },
658 Self::Close => Self::Close,
659 }
660 }
661}
662
663#[derive(Debug, Clone, PartialEq)]
664pub struct PaintPath {
665 pub verbs: Vec<PathVerb>,
666 pub fill: Option<PaintBrush>,
667 pub stroke: Option<AlignedStroke>,
668 pub stroke_options: PathStrokeOptions,
669 pub fill_rule: PathFillRule,
670}
671
672impl PaintPath {
673 pub fn new() -> Self {
674 Self {
675 verbs: Vec::new(),
676 fill: None,
677 stroke: None,
678 stroke_options: PathStrokeOptions::default(),
679 fill_rule: PathFillRule::default(),
680 }
681 }
682
683 pub fn move_to(mut self, point: UiPoint) -> Self {
684 self.verbs.push(PathVerb::MoveTo(point));
685 self
686 }
687
688 pub fn line_to(mut self, point: UiPoint) -> Self {
689 self.verbs.push(PathVerb::LineTo(point));
690 self
691 }
692
693 pub fn quadratic_to(mut self, control: UiPoint, to: UiPoint) -> Self {
694 self.verbs.push(PathVerb::QuadraticTo { control, to });
695 self
696 }
697
698 pub fn cubic_to(mut self, control_a: UiPoint, control_b: UiPoint, to: UiPoint) -> Self {
699 self.verbs.push(PathVerb::CubicTo {
700 control_a,
701 control_b,
702 to,
703 });
704 self
705 }
706
707 pub fn close(mut self) -> Self {
708 self.verbs.push(PathVerb::Close);
709 self
710 }
711
712 pub fn fill(mut self, fill: impl Into<PaintBrush>) -> Self {
713 self.fill = Some(fill.into());
714 self
715 }
716
717 pub const fn fill_rule(mut self, fill_rule: PathFillRule) -> Self {
718 self.fill_rule = fill_rule;
719 self
720 }
721
722 pub fn stroke(mut self, stroke: impl Into<AlignedStroke>) -> Self {
723 let stroke = stroke.into();
724 self.stroke = stroke.is_visible().then_some(stroke);
725 self
726 }
727
728 pub const fn stroke_options(mut self, options: PathStrokeOptions) -> Self {
729 self.stroke_options = options;
730 self
731 }
732
733 pub const fn line_cap(mut self, line_cap: StrokeLineCap) -> Self {
734 self.stroke_options.line_cap = line_cap;
735 self
736 }
737
738 pub const fn line_join(mut self, line_join: StrokeLineJoin) -> Self {
739 self.stroke_options.line_join = line_join;
740 self
741 }
742
743 pub const fn miter_limit(mut self, miter_limit: f32) -> Self {
744 self.stroke_options.miter_limit = miter_limit;
745 self
746 }
747
748 pub fn translated(mut self, offset: UiPoint) -> Self {
749 self.verbs = self
750 .verbs
751 .into_iter()
752 .map(|verb| verb.translated(offset))
753 .collect();
754 if let Some(fill) = &self.fill {
755 self.fill = Some(fill.translated(offset));
756 }
757 self
758 }
759
760 pub fn pixel_snapped(mut self, policy: PixelSnapPolicy) -> Self {
761 self.verbs = self
762 .verbs
763 .into_iter()
764 .map(|verb| verb.pixel_snapped(policy))
765 .collect();
766 if let Some(stroke) = self.stroke {
767 self.stroke = Some(AlignedStroke {
768 style: policy.snap_stroke(stroke.style),
769 alignment: stroke.alignment,
770 });
771 }
772 self
773 }
774
775 pub fn bounds(&self) -> UiRect {
776 let mut points = Vec::new();
777 for verb in &self.verbs {
778 match *verb {
779 PathVerb::MoveTo(point) | PathVerb::LineTo(point) => points.push(point),
780 PathVerb::QuadraticTo { control, to } => {
781 points.push(control);
782 points.push(to);
783 }
784 PathVerb::CubicTo {
785 control_a,
786 control_b,
787 to,
788 } => {
789 points.push(control_a);
790 points.push(control_b);
791 points.push(to);
792 }
793 PathVerb::Close => {}
794 }
795 }
796
797 rect_from_points(&points)
798 }
799
800 pub fn flattened_points(&self, tolerance: f32) -> Vec<UiPoint> {
801 self.flattened_contours(tolerance)
802 .into_iter()
803 .flatten()
804 .collect()
805 }
806
807 pub fn flattened_contours(&self, tolerance: f32) -> Vec<Vec<UiPoint>> {
808 let tolerance = if tolerance.is_finite() && tolerance > 0.0 {
809 tolerance
810 } else {
811 1.0
812 };
813 let mut contours = Vec::<Vec<UiPoint>>::new();
814 let mut points = Vec::<UiPoint>::new();
815 let mut current = None;
816 let mut contour_start = None;
817 for verb in &self.verbs {
818 match *verb {
819 PathVerb::MoveTo(point) => {
820 if !points.is_empty() {
821 contours.push(std::mem::take(&mut points));
822 }
823 points.push(point);
824 current = Some(point);
825 contour_start = Some(point);
826 }
827 PathVerb::LineTo(point) => {
828 points.push(point);
829 current = Some(point);
830 }
831 PathVerb::QuadraticTo { control, to } => {
832 let Some(from) = current else {
833 points.push(to);
834 current = Some(to);
835 contour_start.get_or_insert(to);
836 continue;
837 };
838 let segments = quadratic_segments(from, control, to, tolerance);
839 for index in 1..=segments {
840 let t = index as f32 / segments as f32;
841 points.push(quadratic_point(from, control, to, t));
842 }
843 current = Some(to);
844 }
845 PathVerb::CubicTo {
846 control_a,
847 control_b,
848 to,
849 } => {
850 let Some(from) = current else {
851 points.push(to);
852 current = Some(to);
853 contour_start.get_or_insert(to);
854 continue;
855 };
856 let segments = cubic_segments(from, control_a, control_b, to, tolerance);
857 for index in 1..=segments {
858 let t = index as f32 / segments as f32;
859 points.push(cubic_point(from, control_a, control_b, to, t));
860 }
861 current = Some(to);
862 }
863 PathVerb::Close => {
864 if let (Some(start), Some(last)) = (contour_start, current) {
865 if start != last {
866 points.push(start);
867 }
868 }
869 if !points.is_empty() {
870 contours.push(std::mem::take(&mut points));
871 }
872 current = contour_start;
873 contour_start = None;
874 }
875 }
876 }
877 if !points.is_empty() {
878 contours.push(points);
879 }
880 contours
881 }
882
883 pub fn is_closed(&self) -> bool {
884 self.verbs
885 .iter()
886 .any(|verb| matches!(verb, PathVerb::Close))
887 }
888
889 pub fn tessellated_fill(&self, tolerance: f32) -> Vec<[UiPoint; 3]> {
890 let path = self.to_lyon_path();
891 let mut buffers: VertexBuffers<lyon_tessellation::math::Point, u16> = VertexBuffers::new();
892 let mut tessellator = FillTessellator::new();
893 let options = FillOptions::tolerance(finite_positive_or(tolerance, 1.0)).with_fill_rule(
894 match self.fill_rule {
895 PathFillRule::NonZero => LyonFillRule::NonZero,
896 PathFillRule::EvenOdd => LyonFillRule::EvenOdd,
897 },
898 );
899 if tessellator
900 .tessellate_path(&path, &options, &mut simple_builder(&mut buffers))
901 .is_err()
902 {
903 return tessellate_polygon(&self.flattened_points(tolerance));
904 }
905 vertex_buffers_to_triangles(buffers)
906 }
907
908 pub fn tessellated_stroke(&self, tolerance: f32) -> Vec<[UiPoint; 3]> {
909 let Some(stroke) = self.stroke else {
910 return Vec::new();
911 };
912 let path = self.to_lyon_path();
913 let mut buffers: VertexBuffers<lyon_tessellation::math::Point, u16> = VertexBuffers::new();
914 let mut tessellator = StrokeTessellator::new();
915 let options = StrokeOptions::tolerance(finite_positive_or(tolerance, 1.0))
916 .with_line_width(stroke.style.width.max(1.0))
917 .with_line_cap(match self.stroke_options.line_cap {
918 StrokeLineCap::Butt => LyonLineCap::Butt,
919 StrokeLineCap::Square => LyonLineCap::Square,
920 StrokeLineCap::Round => LyonLineCap::Round,
921 })
922 .with_line_join(match self.stroke_options.line_join {
923 StrokeLineJoin::Miter => LyonLineJoin::Miter,
924 StrokeLineJoin::Bevel => LyonLineJoin::Bevel,
925 StrokeLineJoin::Round => LyonLineJoin::Round,
926 })
927 .with_miter_limit(
928 finite_positive_or(
929 self.stroke_options.miter_limit,
930 PathStrokeOptions::DEFAULT_MITER_LIMIT,
931 )
932 .max(StrokeOptions::MINIMUM_MITER_LIMIT),
933 );
934 if tessellator
935 .tessellate_path(&path, &options, &mut simple_builder(&mut buffers))
936 .is_err()
937 {
938 return tessellate_polyline_stroke(
939 &self.flattened_points(tolerance),
940 stroke.style,
941 self.stroke_options,
942 self.is_closed(),
943 );
944 }
945 vertex_buffers_to_triangles(buffers)
946 }
947
948 fn to_lyon_path(&self) -> LyonPath {
949 let mut builder = LyonPath::builder().with_svg();
950 for verb in &self.verbs {
951 match *verb {
952 PathVerb::MoveTo(point) => {
953 builder.move_to(to_lyon_point(point));
954 }
955 PathVerb::LineTo(point) => {
956 builder.line_to(to_lyon_point(point));
957 }
958 PathVerb::QuadraticTo { control, to } => {
959 builder.quadratic_bezier_to(to_lyon_point(control), to_lyon_point(to));
960 }
961 PathVerb::CubicTo {
962 control_a,
963 control_b,
964 to,
965 } => {
966 builder.cubic_bezier_to(
967 to_lyon_point(control_a),
968 to_lyon_point(control_b),
969 to_lyon_point(to),
970 );
971 }
972 PathVerb::Close => {
973 builder.close();
974 }
975 }
976 }
977 builder.build()
978 }
979}
980
981impl Default for PaintPath {
982 fn default() -> Self {
983 Self::new()
984 }
985}
986
987fn to_lyon_point(point: UiPoint) -> lyon_tessellation::math::Point {
988 lyon_point(point.x, point.y)
989}
990
991fn from_lyon_point(point: lyon_tessellation::math::Point) -> UiPoint {
992 UiPoint::new(point.x, point.y)
993}
994
995fn vertex_buffers_to_triangles(
996 buffers: VertexBuffers<lyon_tessellation::math::Point, u16>,
997) -> Vec<[UiPoint; 3]> {
998 buffers
999 .indices
1000 .chunks_exact(3)
1001 .filter_map(|indices| {
1002 let a = buffers.vertices.get(usize::from(indices[0]))?;
1003 let b = buffers.vertices.get(usize::from(indices[1]))?;
1004 let c = buffers.vertices.get(usize::from(indices[2]))?;
1005 Some([
1006 from_lyon_point(*a),
1007 from_lyon_point(*b),
1008 from_lyon_point(*c),
1009 ])
1010 })
1011 .collect()
1012}
1013
1014fn tessellate_polygon(points: &[UiPoint]) -> Vec<[UiPoint; 3]> {
1015 let mut polygon = sanitize_polygon(points);
1016 if polygon.len() < 3 {
1017 return Vec::new();
1018 }
1019 if signed_area(&polygon) < 0.0 {
1020 polygon.reverse();
1021 }
1022
1023 let mut indices = (0..polygon.len()).collect::<Vec<_>>();
1024 let mut triangles = Vec::with_capacity(polygon.len().saturating_sub(2));
1025 let mut guard = 0usize;
1026 while indices.len() > 3 && guard < polygon.len().saturating_mul(polygon.len()) {
1027 guard += 1;
1028 let mut clipped = false;
1029 for index in 0..indices.len() {
1030 let previous = indices[(index + indices.len() - 1) % indices.len()];
1031 let current = indices[index];
1032 let next = indices[(index + 1) % indices.len()];
1033 let a = polygon[previous];
1034 let b = polygon[current];
1035 let c = polygon[next];
1036 if cross(sub_points(b, a), sub_points(c, b)) <= 0.0 {
1037 continue;
1038 }
1039 if indices.iter().copied().any(|candidate| {
1040 candidate != previous
1041 && candidate != current
1042 && candidate != next
1043 && point_in_triangle(polygon[candidate], a, b, c)
1044 }) {
1045 continue;
1046 }
1047 triangles.push([a, b, c]);
1048 indices.remove(index);
1049 clipped = true;
1050 break;
1051 }
1052 if !clipped {
1053 return polygon_fan_triangles(&polygon);
1054 }
1055 }
1056
1057 if indices.len() == 3 {
1058 triangles.push([
1059 polygon[indices[0]],
1060 polygon[indices[1]],
1061 polygon[indices[2]],
1062 ]);
1063 }
1064 triangles
1065}
1066
1067fn tessellate_polyline_stroke(
1068 points: &[UiPoint],
1069 stroke: StrokeStyle,
1070 options: PathStrokeOptions,
1071 closed: bool,
1072) -> Vec<[UiPoint; 3]> {
1073 if !stroke.is_visible() {
1074 return Vec::new();
1075 }
1076 let points = sanitize_polyline(points);
1077 if points.is_empty() {
1078 return Vec::new();
1079 }
1080 let width = stroke.width.max(1.0);
1081 let half = width * 0.5 + 0.75;
1082 if points.len() == 1 {
1083 return circle_triangles(points[0], half);
1084 }
1085
1086 let mut triangles = Vec::new();
1087 let segment_count = if closed {
1088 points.len()
1089 } else {
1090 points.len() - 1
1091 };
1092 let mut directions = Vec::with_capacity(segment_count);
1093 let mut normals = Vec::with_capacity(segment_count);
1094 for index in 0..segment_count {
1095 let from = points[index];
1096 let to = points[(index + 1) % points.len()];
1097 let direction = normalize(sub_points(to, from));
1098 directions.push(direction);
1099 normals.push(UiPoint::new(-direction.y, direction.x));
1100 }
1101
1102 for index in 0..segment_count {
1103 let mut from = points[index];
1104 let mut to = points[(index + 1) % points.len()];
1105 if !closed {
1106 if index == 0 && options.line_cap == StrokeLineCap::Square {
1107 from = add_points(from, scale_point(directions[index], -half));
1108 }
1109 if index == segment_count - 1 && options.line_cap == StrokeLineCap::Square {
1110 to = add_points(to, scale_point(directions[index], half));
1111 }
1112 }
1113 push_stroke_quad(&mut triangles, from, to, normals[index], half);
1114 }
1115
1116 if closed {
1117 for (index, point) in points.iter().copied().enumerate() {
1118 let previous = (index + segment_count - 1) % segment_count;
1119 let next = index % segment_count;
1120 push_join_triangles(
1121 &mut triangles,
1122 point,
1123 normals[previous],
1124 normals[next],
1125 half,
1126 options,
1127 );
1128 }
1129 } else {
1130 if options.line_cap == StrokeLineCap::Round {
1131 triangles.extend(circle_triangles(points[0], half));
1132 triangles.extend(circle_triangles(points[points.len() - 1], half));
1133 }
1134 for index in 1..points.len() - 1 {
1135 push_join_triangles(
1136 &mut triangles,
1137 points[index],
1138 normals[index - 1],
1139 normals[index],
1140 half,
1141 options,
1142 );
1143 }
1144 }
1145
1146 triangles
1147}
1148
1149fn translated_point(point: UiPoint, offset: UiPoint) -> UiPoint {
1150 UiPoint::new(point.x + offset.x, point.y + offset.y)
1151}
1152
1153fn rect_from_points(points: &[UiPoint]) -> UiRect {
1154 if points.is_empty() {
1155 return UiRect::new(0.0, 0.0, 0.0, 0.0);
1156 }
1157
1158 let mut left = points[0].x;
1159 let mut top = points[0].y;
1160 let mut right = points[0].x;
1161 let mut bottom = points[0].y;
1162 for point in points.iter().copied().skip(1) {
1163 left = left.min(point.x);
1164 top = top.min(point.y);
1165 right = right.max(point.x);
1166 bottom = bottom.max(point.y);
1167 }
1168
1169 UiRect::new(left, top, right - left, bottom - top)
1170}
1171
1172fn point_distance(left: UiPoint, right: UiPoint) -> f32 {
1173 let dx = right.x - left.x;
1174 let dy = right.y - left.y;
1175 (dx * dx + dy * dy).sqrt()
1176}
1177
1178fn quadratic_segments(from: UiPoint, control: UiPoint, to: UiPoint, tolerance: f32) -> usize {
1179 let length = point_distance(from, control) + point_distance(control, to);
1180 ((length / tolerance).ceil() as usize).clamp(4, 64)
1181}
1182
1183fn cubic_segments(
1184 from: UiPoint,
1185 control_a: UiPoint,
1186 control_b: UiPoint,
1187 to: UiPoint,
1188 tolerance: f32,
1189) -> usize {
1190 let length = point_distance(from, control_a)
1191 + point_distance(control_a, control_b)
1192 + point_distance(control_b, to);
1193 ((length / tolerance).ceil() as usize).clamp(6, 96)
1194}
1195
1196fn quadratic_point(from: UiPoint, control: UiPoint, to: UiPoint, t: f32) -> UiPoint {
1197 let inverse = 1.0 - t;
1198 UiPoint::new(
1199 inverse * inverse * from.x + 2.0 * inverse * t * control.x + t * t * to.x,
1200 inverse * inverse * from.y + 2.0 * inverse * t * control.y + t * t * to.y,
1201 )
1202}
1203
1204fn cubic_point(
1205 from: UiPoint,
1206 control_a: UiPoint,
1207 control_b: UiPoint,
1208 to: UiPoint,
1209 t: f32,
1210) -> UiPoint {
1211 let inverse = 1.0 - t;
1212 UiPoint::new(
1213 inverse * inverse * inverse * from.x
1214 + 3.0 * inverse * inverse * t * control_a.x
1215 + 3.0 * inverse * t * t * control_b.x
1216 + t * t * t * to.x,
1217 inverse * inverse * inverse * from.y
1218 + 3.0 * inverse * inverse * t * control_a.y
1219 + 3.0 * inverse * t * t * control_b.y
1220 + t * t * t * to.y,
1221 )
1222}
1223
1224fn sanitize_polygon(points: &[UiPoint]) -> Vec<UiPoint> {
1225 let mut clean = sanitize_polyline(points);
1226 if clean.len() > 1 && clean.first() == clean.last() {
1227 clean.pop();
1228 }
1229 clean
1230}
1231
1232fn sanitize_polyline(points: &[UiPoint]) -> Vec<UiPoint> {
1233 let mut clean = Vec::with_capacity(points.len());
1234 for point in points.iter().copied() {
1235 if point.x.is_finite() && point.y.is_finite() && clean.last() != Some(&point) {
1236 clean.push(point);
1237 }
1238 }
1239 clean
1240}
1241
1242fn polygon_fan_triangles(points: &[UiPoint]) -> Vec<[UiPoint; 3]> {
1243 if points.len() < 3 {
1244 return Vec::new();
1245 }
1246 let mut triangles = Vec::with_capacity(points.len().saturating_sub(2));
1247 for index in 1..points.len() - 1 {
1248 triangles.push([points[0], points[index], points[index + 1]]);
1249 }
1250 triangles
1251}
1252
1253fn signed_area(points: &[UiPoint]) -> f32 {
1254 let mut area = 0.0;
1255 for index in 0..points.len() {
1256 let next = (index + 1) % points.len();
1257 area += points[index].x * points[next].y - points[next].x * points[index].y;
1258 }
1259 area * 0.5
1260}
1261
1262fn point_in_triangle(point: UiPoint, a: UiPoint, b: UiPoint, c: UiPoint) -> bool {
1263 let ab = cross(sub_points(b, a), sub_points(point, a));
1264 let bc = cross(sub_points(c, b), sub_points(point, b));
1265 let ca = cross(sub_points(a, c), sub_points(point, c));
1266 (ab >= -f32::EPSILON && bc >= -f32::EPSILON && ca >= -f32::EPSILON)
1267 || (ab <= f32::EPSILON && bc <= f32::EPSILON && ca <= f32::EPSILON)
1268}
1269
1270fn push_stroke_quad(
1271 triangles: &mut Vec<[UiPoint; 3]>,
1272 from: UiPoint,
1273 to: UiPoint,
1274 normal: UiPoint,
1275 half_width: f32,
1276) {
1277 let offset = scale_point(normal, half_width);
1278 let a = add_points(from, offset);
1279 let b = add_points(to, offset);
1280 let c = sub_points(to, offset);
1281 let d = sub_points(from, offset);
1282 triangles.push([a, b, c]);
1283 triangles.push([a, c, d]);
1284}
1285
1286fn push_join_triangles(
1287 triangles: &mut Vec<[UiPoint; 3]>,
1288 point: UiPoint,
1289 previous_normal: UiPoint,
1290 next_normal: UiPoint,
1291 half_width: f32,
1292 options: PathStrokeOptions,
1293) {
1294 match options.line_join {
1295 StrokeLineJoin::Round => triangles.extend(circle_triangles(point, half_width)),
1296 StrokeLineJoin::Bevel => {
1297 push_bevel_join(triangles, point, previous_normal, next_normal, half_width);
1298 }
1299 StrokeLineJoin::Miter => {
1300 if !push_miter_join(
1301 triangles,
1302 point,
1303 previous_normal,
1304 next_normal,
1305 half_width,
1306 options.miter_limit,
1307 ) {
1308 push_bevel_join(triangles, point, previous_normal, next_normal, half_width);
1309 }
1310 }
1311 }
1312}
1313
1314fn push_bevel_join(
1315 triangles: &mut Vec<[UiPoint; 3]>,
1316 point: UiPoint,
1317 previous_normal: UiPoint,
1318 next_normal: UiPoint,
1319 half_width: f32,
1320) {
1321 triangles.push([
1322 point,
1323 add_points(point, scale_point(previous_normal, half_width)),
1324 add_points(point, scale_point(next_normal, half_width)),
1325 ]);
1326 triangles.push([
1327 point,
1328 sub_points(point, scale_point(previous_normal, half_width)),
1329 sub_points(point, scale_point(next_normal, half_width)),
1330 ]);
1331}
1332
1333fn push_miter_join(
1334 triangles: &mut Vec<[UiPoint; 3]>,
1335 point: UiPoint,
1336 previous_normal: UiPoint,
1337 next_normal: UiPoint,
1338 half_width: f32,
1339 miter_limit: f32,
1340) -> bool {
1341 let Some(miter) = try_miter(previous_normal, next_normal, half_width, miter_limit) else {
1342 return false;
1343 };
1344 let previous = add_points(point, scale_point(previous_normal, half_width));
1345 let next = add_points(point, scale_point(next_normal, half_width));
1346 let tip = add_points(point, miter);
1347 triangles.push([previous, tip, next]);
1348
1349 let previous = sub_points(point, scale_point(previous_normal, half_width));
1350 let next = sub_points(point, scale_point(next_normal, half_width));
1351 let tip = sub_points(point, miter);
1352 triangles.push([previous, next, tip]);
1353 true
1354}
1355
1356fn try_miter(
1357 previous_normal: UiPoint,
1358 next_normal: UiPoint,
1359 half_width: f32,
1360 miter_limit: f32,
1361) -> Option<UiPoint> {
1362 let sum = add_points(previous_normal, next_normal);
1363 let miter = normalize(sum);
1364 if vector_length(miter) <= f32::EPSILON {
1365 return None;
1366 }
1367 let denominator = dot(miter, next_normal);
1368 if denominator.abs() <= 0.01 {
1369 return None;
1370 }
1371 let length = half_width / denominator;
1372 let max_length =
1373 half_width * finite_positive_or(miter_limit, PathStrokeOptions::DEFAULT_MITER_LIMIT);
1374 if length.abs() > max_length {
1375 return None;
1376 }
1377 Some(scale_point(miter, length))
1378}
1379
1380fn circle_triangles(center: UiPoint, radius: f32) -> Vec<[UiPoint; 3]> {
1381 if radius <= 0.0 {
1382 return Vec::new();
1383 }
1384 let segments = ((radius * 4.0).ceil() as usize).clamp(12, 48);
1385 let mut triangles = Vec::with_capacity(segments);
1386 for index in 0..segments {
1387 let a0 = std::f32::consts::TAU * index as f32 / segments as f32;
1388 let a1 = std::f32::consts::TAU * (index + 1) as f32 / segments as f32;
1389 triangles.push([
1390 center,
1391 UiPoint::new(center.x + radius * a0.cos(), center.y + radius * a0.sin()),
1392 UiPoint::new(center.x + radius * a1.cos(), center.y + radius * a1.sin()),
1393 ]);
1394 }
1395 triangles
1396}
1397
1398fn add_points(left: UiPoint, right: UiPoint) -> UiPoint {
1399 UiPoint::new(left.x + right.x, left.y + right.y)
1400}
1401
1402fn sub_points(left: UiPoint, right: UiPoint) -> UiPoint {
1403 UiPoint::new(left.x - right.x, left.y - right.y)
1404}
1405
1406fn scale_point(point: UiPoint, scale: f32) -> UiPoint {
1407 UiPoint::new(point.x * scale, point.y * scale)
1408}
1409
1410fn dot(left: UiPoint, right: UiPoint) -> f32 {
1411 left.x * right.x + left.y * right.y
1412}
1413
1414fn cross(left: UiPoint, right: UiPoint) -> f32 {
1415 left.x * right.y - left.y * right.x
1416}
1417
1418fn vector_length(point: UiPoint) -> f32 {
1419 (point.x * point.x + point.y * point.y).sqrt()
1420}
1421
1422fn normalize(point: UiPoint) -> UiPoint {
1423 let length = vector_length(point);
1424 if length <= f32::EPSILON {
1425 UiPoint::new(0.0, 0.0)
1426 } else {
1427 UiPoint::new(point.x / length, point.y / length)
1428 }
1429}
1430
1431fn finite_positive_or(value: f32, fallback: f32) -> f32 {
1432 if value.is_finite() && value > 0.0 {
1433 value
1434 } else {
1435 fallback
1436 }
1437}
1438
1439#[cfg(test)]
1440mod tests {
1441 use super::*;
1442
1443 #[test]
1444 fn pixel_snap_policy_maps_values_rects_and_hairline_segments() {
1445 let policy = PixelSnapPolicy::new(2.0);
1446
1447 assert!(policy.enabled());
1448 assert_eq!(policy.pixel_size(), 0.5);
1449 assert_eq!(policy.snap_value(10.26), 10.5);
1450 assert_eq!(policy.snap_center_value(10.26), 10.25);
1451 assert_eq!(
1452 policy.snap_point(UiPoint::new(0.24, 0.26)),
1453 UiPoint::new(0.0, 0.5)
1454 );
1455 assert_eq!(
1456 policy.snap_rect(UiRect::new(0.24, 0.26, 10.51, 4.49)),
1457 UiRect::new(0.0, 0.5, 11.0, 4.5)
1458 );
1459
1460 let (from, to) = PixelSnapPolicy::new(1.0)
1461 .snap_line_segment(UiPoint::new(10.1, 0.2), UiPoint::new(10.1, 9.8));
1462 assert_eq!(from, UiPoint::new(10.5, 0.0));
1463 assert_eq!(to, UiPoint::new(10.5, 10.0));
1464
1465 let (from, to) = PixelSnapPolicy::new(1.0)
1466 .snap_line_segment(UiPoint::new(0.2, 5.1), UiPoint::new(9.8, 5.1));
1467 assert_eq!(from, UiPoint::new(0.0, 5.5));
1468 assert_eq!(to, UiPoint::new(10.0, 5.5));
1469 }
1470
1471 #[test]
1472 fn pixel_snap_policy_preserves_disabled_and_snaps_stroke_widths_up() {
1473 let disabled = PixelSnapPolicy::disabled();
1474 assert!(!disabled.enabled());
1475 assert_eq!(disabled.snap_value(10.26), 10.26);
1476 assert_eq!(PixelSnapPolicy::new(f32::NAN), PixelSnapPolicy::DISABLED);
1477
1478 let policy = PixelSnapPolicy::new(2.0);
1479 assert_eq!(policy.snap_stroke_width(0.1), 0.5);
1480 assert_eq!(policy.snap_stroke_width(1.2), 1.5);
1481 assert_eq!(policy.snap_stroke_width(0.0), 0.0);
1482 }
1483
1484 #[test]
1485 fn paint_rect_and_path_can_be_pixel_snapped() {
1486 let policy = PixelSnapPolicy::new(2.0);
1487 let rect = PaintRect::solid(UiRect::new(1.24, 2.26, 10.51, 4.49), ColorRgba::WHITE)
1488 .stroke(AlignedStroke::inside(StrokeStyle::new(
1489 ColorRgba::WHITE,
1490 0.3,
1491 )))
1492 .pixel_snapped(policy);
1493
1494 assert_eq!(rect.rect, UiRect::new(1.0, 2.5, 11.0, 4.5));
1495 assert_eq!(rect.stroke.unwrap().style.width, 0.5);
1496
1497 let path = PaintPath::new()
1498 .move_to(UiPoint::new(0.24, 0.26))
1499 .line_to(UiPoint::new(4.74, 3.24))
1500 .stroke(StrokeStyle::new(ColorRgba::WHITE, 0.2))
1501 .pixel_snapped(policy);
1502
1503 assert_eq!(
1504 path.verbs,
1505 vec![
1506 PathVerb::MoveTo(UiPoint::new(0.0, 0.5)),
1507 PathVerb::LineTo(UiPoint::new(4.5, 3.0))
1508 ]
1509 );
1510 assert_eq!(path.stroke.unwrap().style.width, 0.5);
1511 }
1512
1513 #[test]
1514 fn zero_width_strokes_are_not_published_as_visible_paint() {
1515 let rect = PaintRect::solid(UiRect::new(0.0, 0.0, 10.0, 10.0), ColorRgba::WHITE).stroke(
1516 AlignedStroke::inside(StrokeStyle::new(ColorRgba::WHITE, 0.0)),
1517 );
1518 assert_eq!(rect.stroke, None);
1519
1520 let path = PaintPath::new()
1521 .move_to(UiPoint::new(0.0, 0.0))
1522 .line_to(UiPoint::new(10.0, 0.0))
1523 .stroke(StrokeStyle::new(ColorRgba::WHITE, 0.0));
1524 assert_eq!(path.stroke, None);
1525 assert!(path.tessellated_stroke(1.0).is_empty());
1526 }
1527
1528 #[test]
1529 fn paint_path_flattens_quadratic_and_cubic_curves() {
1530 let path = PaintPath::new()
1531 .move_to(UiPoint::new(0.0, 10.0))
1532 .quadratic_to(UiPoint::new(10.0, 0.0), UiPoint::new(20.0, 10.0))
1533 .cubic_to(
1534 UiPoint::new(28.0, 18.0),
1535 UiPoint::new(34.0, 18.0),
1536 UiPoint::new(40.0, 10.0),
1537 );
1538
1539 let points = path.flattened_points(4.0);
1540
1541 assert!(points.len() > 6);
1542 assert_eq!(points.first(), Some(&UiPoint::new(0.0, 10.0)));
1543 assert_eq!(points.last(), Some(&UiPoint::new(40.0, 10.0)));
1544 assert!(
1545 points.iter().any(|point| point.y < 8.0),
1546 "quadratic control point should affect flattened curve"
1547 );
1548 assert!(
1549 points.iter().any(|point| point.y > 12.0),
1550 "cubic control points should affect flattened curve"
1551 );
1552 }
1553
1554 #[test]
1555 fn paint_path_preserves_contours_and_stroke_options() {
1556 let path = PaintPath::new()
1557 .move_to(UiPoint::new(0.0, 0.0))
1558 .line_to(UiPoint::new(8.0, 0.0))
1559 .move_to(UiPoint::new(0.0, 8.0))
1560 .line_to(UiPoint::new(8.0, 8.0))
1561 .stroke(StrokeStyle::new(ColorRgba::WHITE, 2.0))
1562 .line_cap(StrokeLineCap::Butt)
1563 .line_join(StrokeLineJoin::Miter)
1564 .miter_limit(2.0);
1565
1566 let contours = path.flattened_contours(1.0);
1567 assert_eq!(contours.len(), 2);
1568 assert_eq!(path.stroke_options.line_cap, StrokeLineCap::Butt);
1569 assert_eq!(path.stroke_options.line_join, StrokeLineJoin::Miter);
1570 assert_eq!(path.stroke_options.miter_limit, 2.0);
1571 }
1572
1573 #[test]
1574 fn tessellators_cover_non_convex_fill_and_configurable_strokes() {
1575 let polygon = [
1576 UiPoint::new(0.0, 0.0),
1577 UiPoint::new(16.0, 0.0),
1578 UiPoint::new(16.0, 16.0),
1579 UiPoint::new(8.0, 8.0),
1580 UiPoint::new(0.0, 16.0),
1581 ];
1582 assert!(tessellate_polygon(&polygon).len() >= 3);
1583
1584 let polyline = [
1585 UiPoint::new(0.0, 0.0),
1586 UiPoint::new(12.0, 0.0),
1587 UiPoint::new(12.0, 12.0),
1588 ];
1589 let butt = tessellate_polyline_stroke(
1590 &polyline,
1591 StrokeStyle::new(ColorRgba::WHITE, 3.0),
1592 PathStrokeOptions::new().line_cap(StrokeLineCap::Butt),
1593 false,
1594 );
1595 let round = tessellate_polyline_stroke(
1596 &polyline,
1597 StrokeStyle::new(ColorRgba::WHITE, 3.0),
1598 PathStrokeOptions::new()
1599 .line_cap(StrokeLineCap::Round)
1600 .line_join(StrokeLineJoin::Round),
1601 false,
1602 );
1603 assert!(round.len() > butt.len(), "round={round:?} butt={butt:?}");
1604 }
1605}