renderling/ui/cpu/
path.rs

1//! Path and builder.
2//!
3//! Path colors are sRGB.
4use crate::{geometry::Vertex, material::Material, primitive::Primitive};
5use glam::{Vec2, Vec3, Vec3Swizzles, Vec4};
6use lyon::{
7    path::traits::PathBuilder,
8    tessellation::{
9        BuffersBuilder, FillTessellator, FillVertex, StrokeTessellator, StrokeVertex, VertexBuffers,
10    },
11};
12
13use super::{ImageId, Ui, UiTransform};
14pub use lyon::tessellation::{LineCap, LineJoin};
15
16pub struct UiPath {
17    pub transform: UiTransform,
18    pub material: Material,
19    pub primitive: Primitive,
20}
21
22#[derive(Clone, Copy)]
23struct PathAttributes {
24    stroke_color: Vec4,
25    fill_color: Vec4,
26}
27
28impl Default for PathAttributes {
29    fn default() -> Self {
30        Self {
31            stroke_color: Vec4::ONE,
32            fill_color: Vec4::new(0.2, 0.2, 0.2, 1.0),
33        }
34    }
35}
36
37impl PathAttributes {
38    const NUM_ATTRIBUTES: usize = 8;
39
40    fn to_array(self) -> [f32; Self::NUM_ATTRIBUTES] {
41        [
42            self.stroke_color.x,
43            self.stroke_color.y,
44            self.stroke_color.z,
45            self.stroke_color.w,
46            self.fill_color.x,
47            self.fill_color.y,
48            self.fill_color.z,
49            self.fill_color.w,
50        ]
51    }
52
53    fn from_slice(s: &[f32]) -> Self {
54        Self {
55            stroke_color: Vec4::new(s[0], s[1], s[2], s[3]),
56            fill_color: Vec4::new(s[4], s[5], s[6], s[7]),
57        }
58    }
59}
60
61#[derive(Clone, Copy, Debug)]
62pub struct StrokeOptions {
63    pub line_width: f32,
64    pub line_cap: LineCap,
65    pub line_join: LineJoin,
66    pub image_id: Option<ImageId>,
67}
68
69impl Default for StrokeOptions {
70    fn default() -> Self {
71        StrokeOptions {
72            line_width: 2.0,
73            line_cap: LineCap::Round,
74            line_join: LineJoin::Round,
75            image_id: None,
76        }
77    }
78}
79
80#[derive(Clone, Copy, Debug, Default)]
81pub struct FillOptions {
82    pub image_id: Option<ImageId>,
83}
84
85#[derive(Clone)]
86pub struct UiPathBuilder {
87    ui: Ui,
88    attributes: PathAttributes,
89    inner: lyon::path::BuilderWithAttributes,
90    default_stroke_options: StrokeOptions,
91    default_fill_options: FillOptions,
92}
93
94fn vec2_to_point(v: impl Into<Vec2>) -> lyon::geom::Point<f32> {
95    let Vec2 { x, y } = v.into();
96    lyon::geom::point(x, y)
97}
98
99fn vec2_to_vec(v: impl Into<Vec2>) -> lyon::geom::Vector<f32> {
100    let Vec2 { x, y } = v.into();
101    lyon::geom::Vector::new(x, y)
102}
103
104impl UiPathBuilder {
105    pub fn new(ui: &Ui) -> Self {
106        Self {
107            ui: ui.clone(),
108            attributes: PathAttributes::default(),
109            inner: lyon::path::Path::builder_with_attributes(PathAttributes::NUM_ATTRIBUTES),
110            default_stroke_options: *ui.default_stroke_options.read().unwrap(),
111            default_fill_options: *ui.default_fill_options.read().unwrap(),
112        }
113    }
114
115    pub fn begin(&mut self, at: impl Into<Vec2>) -> &mut Self {
116        self.inner
117            .begin(vec2_to_point(at), &self.attributes.to_array());
118        self
119    }
120
121    pub fn with_begin(mut self, at: impl Into<Vec2>) -> Self {
122        self.begin(at);
123        self
124    }
125
126    pub fn end(&mut self, close: bool) -> &mut Self {
127        self.inner.end(close);
128        self
129    }
130
131    pub fn with_end(mut self, close: bool) -> Self {
132        self.end(close);
133        self
134    }
135
136    pub fn line_to(&mut self, to: impl Into<Vec2>) -> &mut Self {
137        self.inner
138            .line_to(vec2_to_point(to), &self.attributes.to_array());
139        self
140    }
141
142    pub fn with_line_to(mut self, to: impl Into<Vec2>) -> Self {
143        self.line_to(to);
144        self
145    }
146
147    pub fn quadratic_bezier_to(&mut self, ctrl: impl Into<Vec2>, to: impl Into<Vec2>) -> &mut Self {
148        self.inner.quadratic_bezier_to(
149            vec2_to_point(ctrl),
150            vec2_to_point(to),
151            &self.attributes.to_array(),
152        );
153        self
154    }
155
156    pub fn with_quadratic_bezier_to(mut self, ctrl: impl Into<Vec2>, to: impl Into<Vec2>) -> Self {
157        self.quadratic_bezier_to(ctrl, to);
158        self
159    }
160
161    pub fn cubic_bezier_to(
162        &mut self,
163        ctrl1: impl Into<Vec2>,
164        ctrl2: impl Into<Vec2>,
165        to: impl Into<Vec2>,
166    ) -> &mut Self {
167        self.inner.cubic_bezier_to(
168            vec2_to_point(ctrl1),
169            vec2_to_point(ctrl2),
170            vec2_to_point(to),
171            &self.attributes.to_array(),
172        );
173        self
174    }
175
176    pub fn with_cubic_bezier_to(
177        mut self,
178        ctrl1: impl Into<Vec2>,
179        ctrl2: impl Into<Vec2>,
180        to: impl Into<Vec2>,
181    ) -> Self {
182        self.cubic_bezier_to(ctrl1, ctrl2, to);
183        self
184    }
185
186    pub fn add_rectangle(
187        &mut self,
188        box_min: impl Into<Vec2>,
189        box_max: impl Into<Vec2>,
190    ) -> &mut Self {
191        let bx = lyon::geom::Box2D::new(vec2_to_point(box_min), vec2_to_point(box_max));
192        self.inner.add_rectangle(
193            &bx,
194            lyon::path::Winding::Positive,
195            &self.attributes.to_array(),
196        );
197        self
198    }
199
200    pub fn with_rectangle(mut self, box_min: impl Into<Vec2>, box_max: impl Into<Vec2>) -> Self {
201        self.add_rectangle(box_min, box_max);
202        self
203    }
204
205    pub fn add_rounded_rectangle(
206        &mut self,
207        box_min: impl Into<Vec2>,
208        box_max: impl Into<Vec2>,
209        top_left_radius: f32,
210        top_right_radius: f32,
211        bottom_left_radius: f32,
212        bottom_right_radius: f32,
213    ) -> &mut Self {
214        let rect = lyon::geom::Box2D {
215            min: vec2_to_point(box_min),
216            max: vec2_to_point(box_max),
217        };
218        let radii = lyon::path::builder::BorderRadii {
219            top_left: top_left_radius,
220            top_right: top_right_radius,
221            bottom_left: bottom_left_radius,
222            bottom_right: bottom_right_radius,
223        };
224        self.inner.add_rounded_rectangle(
225            &rect,
226            &radii,
227            lyon::path::Winding::Positive,
228            &self.attributes.to_array(),
229        );
230        self
231    }
232
233    pub fn with_rounded_rectangle(
234        mut self,
235        box_min: impl Into<Vec2>,
236        box_max: impl Into<Vec2>,
237        top_left_radius: f32,
238        top_right_radius: f32,
239        bottom_left_radius: f32,
240        bottom_right_radius: f32,
241    ) -> Self {
242        self.add_rounded_rectangle(
243            box_min,
244            box_max,
245            top_left_radius,
246            top_right_radius,
247            bottom_left_radius,
248            bottom_right_radius,
249        );
250        self
251    }
252
253    pub fn add_ellipse(
254        &mut self,
255        center: impl Into<Vec2>,
256        radii: impl Into<Vec2>,
257        rotation: f32,
258    ) -> &mut Self {
259        self.inner.add_ellipse(
260            vec2_to_point(center),
261            vec2_to_vec(radii),
262            lyon::path::math::Angle { radians: rotation },
263            lyon::path::Winding::Positive,
264            &self.attributes.to_array(),
265        );
266        self
267    }
268
269    pub fn with_ellipse(
270        mut self,
271        center: impl Into<Vec2>,
272        radii: impl Into<Vec2>,
273        rotation: f32,
274    ) -> Self {
275        self.add_ellipse(center, radii, rotation);
276        self
277    }
278
279    pub fn add_circle(&mut self, center: impl Into<Vec2>, radius: f32) -> &mut Self {
280        self.inner.add_circle(
281            vec2_to_point(center),
282            radius,
283            lyon::path::Winding::Positive,
284            &self.attributes.to_array(),
285        );
286        self
287    }
288
289    pub fn with_circle(mut self, center: impl Into<Vec2>, radius: f32) -> Self {
290        self.add_circle(center, radius);
291        self
292    }
293
294    pub fn add_polygon(
295        &mut self,
296        is_closed: bool,
297        polygon: impl IntoIterator<Item = Vec2>,
298    ) -> &mut Self {
299        let points = polygon.into_iter().map(vec2_to_point).collect::<Vec<_>>();
300        let polygon = lyon::path::Polygon {
301            points: points.as_slice(),
302            closed: is_closed,
303        };
304        self.inner.add_polygon(polygon, &self.attributes.to_array());
305        self
306    }
307
308    pub fn with_polygon(
309        mut self,
310        is_closed: bool,
311        polygon: impl IntoIterator<Item = Vec2>,
312    ) -> Self {
313        self.add_polygon(is_closed, polygon);
314        self
315    }
316
317    pub fn set_fill_color(&mut self, color: impl Into<Vec4>) -> &mut Self {
318        let mut color = color.into();
319        crate::color::linear_xfer_vec4(&mut color);
320        self.attributes.fill_color = color;
321        self
322    }
323
324    pub fn with_fill_color(mut self, color: impl Into<Vec4>) -> Self {
325        self.set_fill_color(color);
326        self
327    }
328
329    pub fn set_stroke_color(&mut self, color: impl Into<Vec4>) -> &mut Self {
330        let mut color = color.into();
331        crate::color::linear_xfer_vec4(&mut color);
332        self.attributes.stroke_color = color;
333        self
334    }
335
336    pub fn with_stroke_color(mut self, color: impl Into<Vec4>) -> Self {
337        self.set_stroke_color(color);
338        self
339    }
340
341    pub fn fill_with_options(self, options: FillOptions) -> UiPath {
342        let l_path = self.inner.build();
343        let mut geometry = VertexBuffers::<Vertex, u16>::new();
344        let mut tesselator = FillTessellator::new();
345        let material = self.ui.stage.new_material();
346        let mut size = Vec2::ONE;
347        // If we have an image use it in the material
348        if let Some(ImageId(id)) = &options.image_id {
349            let guard = self.ui.images.read().unwrap();
350            if let Some(image) = guard.get(id) {
351                let size_px = image.0.descriptor().size_px;
352                log::debug!("size: {}", size_px);
353                size.x = size_px.x as f32;
354                size.y = size_px.y as f32;
355                material.set_albedo_texture(&image.0);
356            }
357        }
358        tesselator
359            .tessellate_path(
360                l_path.as_slice(),
361                &Default::default(),
362                &mut BuffersBuilder::new(&mut geometry, |mut vertex: FillVertex| {
363                    let p = vertex.position();
364                    let PathAttributes {
365                        stroke_color: _,
366                        fill_color,
367                    } = PathAttributes::from_slice(vertex.interpolated_attributes());
368                    let position = Vec3::new(p.x, p.y, 0.0);
369                    Vertex {
370                        position,
371                        uv0: position.xy() / size,
372                        color: fill_color,
373                        ..Default::default()
374                    }
375                }),
376            )
377            .unwrap();
378        let vertices = self
379            .ui
380            .stage
381            .new_vertices(std::mem::take(&mut geometry.vertices));
382        let indices = self.ui.stage.new_indices(
383            std::mem::take(&mut geometry.indices)
384                .into_iter()
385                .map(|u| u as u32),
386        );
387
388        let transform = self.ui.new_transform();
389        let primitive = self
390            .ui
391            .stage
392            .new_primitive()
393            .with_vertices(&vertices)
394            .with_indices(&indices)
395            .with_material(&material)
396            .with_transform(&transform.transform);
397
398        UiPath {
399            transform,
400            material,
401            primitive,
402        }
403    }
404
405    pub fn fill(self) -> UiPath {
406        let options = self.default_fill_options;
407        self.fill_with_options(options)
408    }
409
410    pub fn stroke_with_options(self, options: StrokeOptions) -> UiPath {
411        let l_path = self.inner.build();
412        let mut geometry = VertexBuffers::<Vertex, u16>::new();
413        let mut tesselator = StrokeTessellator::new();
414        let StrokeOptions {
415            line_width,
416            line_cap,
417            line_join,
418            image_id,
419        } = options;
420        let tesselator_options = lyon::tessellation::StrokeOptions::default()
421            .with_line_cap(line_cap)
422            .with_line_join(line_join)
423            .with_line_width(line_width);
424        let material = self.ui.stage.new_material();
425        let mut size = Vec2::ONE;
426        // If we have an image, use it in the material
427        if let Some(ImageId(id)) = &image_id {
428            let guard = self.ui.images.read().unwrap();
429            if let Some(image) = guard.get(id) {
430                let size_px = image.0.descriptor.get().size_px;
431                log::debug!("size: {}", size_px);
432                size.x = size_px.x as f32;
433                size.y = size_px.y as f32;
434                material.set_albedo_texture(&image.0);
435            }
436        }
437        tesselator
438            .tessellate_path(
439                l_path.as_slice(),
440                &tesselator_options,
441                &mut BuffersBuilder::new(&mut geometry, |mut vertex: StrokeVertex| {
442                    let p = vertex.position();
443                    let PathAttributes {
444                        stroke_color,
445                        fill_color: _,
446                    } = PathAttributes::from_slice(vertex.interpolated_attributes());
447                    let position = Vec3::new(p.x, p.y, 0.0);
448                    Vertex {
449                        position,
450                        uv0: position.xy() / size,
451                        color: stroke_color,
452                        ..Default::default()
453                    }
454                }),
455            )
456            .unwrap();
457        let vertices = self
458            .ui
459            .stage
460            .new_vertices(std::mem::take(&mut geometry.vertices));
461        let indices = self.ui.stage.new_indices(
462            std::mem::take(&mut geometry.indices)
463                .into_iter()
464                .map(|u| u as u32),
465        );
466        let transform = self.ui.new_transform();
467        let renderlet = self
468            .ui
469            .stage
470            .new_primitive()
471            .with_vertices(vertices)
472            .with_indices(indices)
473            .with_transform(&transform.transform)
474            .with_material(&material);
475        UiPath {
476            transform,
477            material,
478            primitive: renderlet,
479        }
480    }
481
482    pub fn stroke(self) -> UiPath {
483        let options = self.default_stroke_options;
484        self.stroke_with_options(options)
485    }
486
487    pub fn fill_and_stroke_with_options(
488        self,
489        fill_options: FillOptions,
490        stroke_options: StrokeOptions,
491    ) -> (UiPath, UiPath) {
492        (
493            self.clone().fill_with_options(fill_options),
494            self.stroke_with_options(stroke_options),
495        )
496    }
497
498    pub fn fill_and_stroke(self) -> (UiPath, UiPath) {
499        let fill_options = self.default_fill_options;
500        let stroke_options = self.default_stroke_options;
501        self.fill_and_stroke_with_options(fill_options, stroke_options)
502    }
503}
504
505#[cfg(test)]
506mod test {
507    use crate::{
508        context::Context,
509        math::hex_to_vec4,
510        test::BlockOnFuture,
511        ui::{
512            test::{cute_beach_palette, Colors},
513            Ui,
514        },
515    };
516    use glam::Vec2;
517
518    use super::*;
519
520    /// Generates points for a star shape.
521    /// `num_points` specifies the number of points (tips) the star will
522    /// have. `radius` specifies the radius of the circle in which
523    /// the star is inscribed.
524    fn star_points(num_points: usize, outer_radius: f32, inner_radius: f32) -> Vec<Vec2> {
525        let mut points = Vec::with_capacity(num_points * 2);
526        let angle_step = std::f32::consts::PI / num_points as f32;
527        for i in 0..num_points * 2 {
528            let angle = angle_step * i as f32;
529            let radius = if i % 2 == 0 {
530                outer_radius
531            } else {
532                inner_radius
533            };
534            points.push(Vec2::new(radius * angle.cos(), radius * angle.sin()));
535        }
536        points
537    }
538
539    #[test]
540    fn can_build_path_sanity() {
541        let ctx = Context::headless(100, 100).block();
542        let ui = Ui::new(&ctx).with_antialiasing(false);
543        let builder = ui
544            .path_builder()
545            .with_fill_color([1.0, 1.0, 0.0, 1.0])
546            .with_stroke_color([0.0, 1.0, 1.0, 1.0])
547            .with_rectangle(Vec2::splat(10.0), Vec2::splat(60.0))
548            .with_circle(Vec2::splat(100.0), 20.0);
549        {
550            let _fill = builder.clone().fill();
551            let _stroke = builder.clone().stroke();
552
553            let frame = ctx.get_next_frame().unwrap();
554            ui.render(&frame.view());
555            let img = frame.read_image().block().unwrap();
556            img_diff::assert_img_eq("ui/path/sanity.png", img);
557        }
558
559        let frame = ctx.get_next_frame().unwrap();
560        ui.render(&frame.view());
561        frame.present();
562
563        {
564            let _resources = builder.fill_and_stroke();
565            let frame = ctx.get_next_frame().unwrap();
566            ui.render(&frame.view());
567            let img = frame.read_image().block().unwrap();
568            img_diff::assert_img_eq_cfg(
569                "ui/path/sanity.png",
570                img,
571                img_diff::DiffCfg {
572                    test_name: Some("ui/path/sanity - separate path and stroke same as together"),
573                    ..Default::default()
574                },
575            );
576        }
577    }
578
579    #[test]
580    fn can_draw_shapes() {
581        let ctx = Context::headless(256, 48).block();
582        let ui = Ui::new(&ctx).with_default_stroke_options(StrokeOptions {
583            line_width: 4.0,
584            ..Default::default()
585        });
586        let mut colors = Colors::from_array(cute_beach_palette());
587
588        // rectangle
589        let fill = colors.next_color();
590        let _rect = ui
591            .path_builder()
592            .with_fill_color(fill)
593            .with_stroke_color(hex_to_vec4(0x333333FF))
594            .with_rectangle(Vec2::splat(2.0), Vec2::splat(42.0))
595            .fill_and_stroke();
596
597        // circle
598        let fill = colors.next_color();
599        let _circ = ui
600            .path_builder()
601            .with_fill_color(fill)
602            .with_stroke_color(hex_to_vec4(0x333333FF))
603            .with_circle([64.0, 22.0], 20.0)
604            .fill_and_stroke();
605
606        // ellipse
607        let fill = colors.next_color();
608        let _elli = ui
609            .path_builder()
610            .with_fill_color(fill)
611            .with_stroke_color(hex_to_vec4(0x333333FF))
612            .with_ellipse([104.0, 22.0], [20.0, 15.0], std::f32::consts::FRAC_PI_4)
613            .fill_and_stroke();
614
615        // various polygons
616        fn circle_points(num_points: usize, radius: f32) -> Vec<Vec2> {
617            let mut points = Vec::with_capacity(num_points);
618            for i in 0..num_points {
619                let angle = 2.0 * std::f32::consts::PI * i as f32 / num_points as f32;
620                points.push(Vec2::new(radius * angle.cos(), radius * angle.sin()));
621            }
622            points
623        }
624
625        let fill = colors.next_color();
626        let center = Vec2::new(144.0, 22.0);
627        let _penta = ui
628            .path_builder()
629            .with_fill_color(fill)
630            .with_stroke_color(hex_to_vec4(0x333333FF))
631            .with_polygon(true, circle_points(5, 20.0).into_iter().map(|p| p + center))
632            .fill_and_stroke();
633
634        let fill = colors.next_color();
635        let center = Vec2::new(184.0, 22.0);
636        let _star = ui
637            .path_builder()
638            .with_fill_color(fill)
639            .with_stroke_color(hex_to_vec4(0x333333FF))
640            .with_polygon(
641                true,
642                star_points(5, 20.0, 10.0).into_iter().map(|p| p + center),
643            )
644            .fill_and_stroke();
645
646        let fill = colors.next_color();
647        let tl = Vec2::new(210.0, 4.0);
648        let _rrect = ui
649            .path_builder()
650            .with_fill_color(fill)
651            .with_stroke_color(hex_to_vec4(0x333333FF))
652            .with_rounded_rectangle(tl, tl + Vec2::new(40.0, 40.0), 5.0, 0.0, 0.0, 10.0)
653            .fill_and_stroke();
654
655        let frame = ctx.get_next_frame().unwrap();
656        ui.render(&frame.view());
657        let img = frame.read_image().block().unwrap();
658        img_diff::assert_img_eq("ui/path/shapes.png", img);
659    }
660
661    #[test]
662    fn can_fill_image() {
663        let w = 150.0;
664        let ctx = Context::headless(w as u32, w as u32).block();
665        let ui = Ui::new(&ctx);
666        let image_id = futures_lite::future::block_on(ui.load_image("../../img/dirt.jpg")).unwrap();
667        let center = Vec2::splat(w / 2.0);
668        let _path = ui
669            .path_builder()
670            .with_polygon(
671                true,
672                star_points(7, w / 2.0, w / 3.0)
673                    .into_iter()
674                    .map(|p| center + p),
675            )
676            .with_fill_color([1.0, 1.0, 1.0, 1.0])
677            .with_stroke_color([1.0, 0.0, 0.0, 1.0])
678            .fill_and_stroke_with_options(
679                FillOptions {
680                    image_id: Some(image_id),
681                },
682                StrokeOptions {
683                    line_width: 5.0,
684                    image_id: Some(image_id),
685                    ..Default::default()
686                },
687            );
688
689        let frame = ctx.get_next_frame().unwrap();
690        ui.render(&frame.view());
691        let mut img = frame.read_srgb_image().block().unwrap();
692        img.pixels_mut().for_each(|p| {
693            crate::color::opto_xfer_u8(&mut p.0[0]);
694            crate::color::opto_xfer_u8(&mut p.0[1]);
695            crate::color::opto_xfer_u8(&mut p.0[2]);
696        });
697        img_diff::assert_img_eq("ui/path/fill_image.png", img);
698    }
699}