1use 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 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 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 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 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 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 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 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}