Skip to main content

operad/render/
paint.rs

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