1use glam::Vec2;
32
33use crate::color::Color;
34use crate::math::Rect;
35use crate::scene::clip::MaskShape;
36use crate::scene::graphics::Fill;
37use crate::scene::transform::Transform;
38
39#[derive(Debug, Clone, PartialEq)]
42#[non_exhaustive]
43pub enum VectorShape {
44 Rect {
46 rect: Rect,
48 },
49 RoundedRect {
51 rect: Rect,
53 radius: f32,
55 },
56 Circle {
58 center: Vec2,
60 radius: f32,
62 },
63 Ellipse {
65 center: Vec2,
67 half_extents: Vec2,
70 },
71 Path {
74 points: Vec<Vec2>,
77 },
78}
79
80impl VectorShape {
81 #[must_use]
83 pub fn rect(rect: Rect) -> Self {
84 Self::Rect { rect }
85 }
86
87 #[must_use]
89 pub fn rounded_rect(rect: Rect, radius: f32) -> Self {
90 Self::RoundedRect { rect, radius }
91 }
92
93 #[must_use]
95 pub fn circle(center: Vec2, radius: f32) -> Self {
96 Self::Circle { center, radius }
97 }
98
99 #[must_use]
101 pub fn ellipse(center: Vec2, half_extents: Vec2) -> Self {
102 Self::Ellipse {
103 center,
104 half_extents,
105 }
106 }
107
108 #[must_use]
110 pub fn path(points: Vec<Vec2>) -> Self {
111 Self::Path { points }
112 }
113
114 #[must_use]
117 pub fn bounds(&self) -> Rect {
118 match self {
119 Self::Rect { rect } | Self::RoundedRect { rect, .. } => *rect,
120 Self::Circle { center, radius } => Rect::new(
121 center.x - radius,
122 center.y - radius,
123 radius * 2.0,
124 radius * 2.0,
125 ),
126 Self::Ellipse {
127 center,
128 half_extents,
129 } => Rect::new(
130 center.x - half_extents.x,
131 center.y - half_extents.y,
132 half_extents.x * 2.0,
133 half_extents.y * 2.0,
134 ),
135 Self::Path { points } => points_bounds(points),
136 }
137 }
138
139 #[must_use]
146 pub fn as_mask_shape(&self) -> Option<MaskShape> {
147 match self {
148 Self::Rect { rect } => Some(MaskShape::Rect { rect: *rect }),
149 Self::RoundedRect { rect, radius } => Some(MaskShape::RoundedRect {
150 rect: *rect,
151 radius: *radius,
152 }),
153 Self::Circle { center, radius } => Some(MaskShape::Circle {
154 center: *center,
155 radius: *radius,
156 }),
157 Self::Ellipse {
158 center,
159 half_extents,
160 } => Some(MaskShape::Ellipse {
161 center: *center,
162 half_extents: *half_extents,
163 }),
164 Self::Path { .. } => None,
165 }
166 }
167
168 #[must_use]
171 pub fn as_path_points(&self) -> Option<&[Vec2]> {
172 match self {
173 Self::Path { points } => Some(points),
174 _ => None,
175 }
176 }
177}
178
179fn points_bounds(points: &[Vec2]) -> Rect {
180 if points.is_empty() {
181 return Rect::new(0.0, 0.0, 0.0, 0.0);
182 }
183 let mut min = points[0];
184 let mut max = points[0];
185 for p in &points[1..] {
186 min = min.min(*p);
187 max = max.max(*p);
188 }
189 Rect::new(min.x, min.y, max.x - min.x, max.y - min.y)
190}
191
192#[derive(Debug, Clone, Copy, PartialEq)]
196pub struct VectorStroke {
197 pub width: f32,
199 pub color: Color,
201}
202
203impl VectorStroke {
204 #[must_use]
206 pub fn new(width: f32, color: Color) -> Self {
207 Self { width, color }
208 }
209}
210
211#[derive(Debug, Clone, PartialEq)]
218pub struct Vector {
219 pub shape: VectorShape,
221 pub fill: Option<Fill>,
223 pub stroke: Option<VectorStroke>,
225 pub opacity: f32,
228 pub transform: Transform,
230}
231
232impl Vector {
233 #[must_use]
236 pub fn new(shape: VectorShape) -> Self {
237 Self {
238 shape,
239 fill: None,
240 stroke: None,
241 opacity: 1.0,
242 transform: Transform::default(),
243 }
244 }
245
246 #[must_use]
248 pub fn with_fill(mut self, fill: Fill) -> Self {
249 self.fill = Some(fill);
250 self
251 }
252
253 #[must_use]
255 pub fn with_stroke(mut self, stroke: VectorStroke) -> Self {
256 self.stroke = Some(stroke);
257 self
258 }
259
260 #[must_use]
263 pub fn with_opacity(mut self, opacity: f32) -> Self {
264 self.opacity = opacity;
265 self
266 }
267
268 #[must_use]
270 pub fn with_transform(mut self, transform: Transform) -> Self {
271 self.transform = transform;
272 self
273 }
274
275 #[must_use]
285 pub fn to_graphics(&self) -> Option<crate::scene::Graphics> {
286 use crate::scene::Graphics;
287 let opacity = self.opacity.clamp(0.0, 1.0);
288 let mut g = Graphics::new();
289 g.container.transform = self.transform;
290 if let Some(fill) = self.fill {
291 g.fill(scale_fill_alpha(fill, opacity));
292 }
293 if let Some(stroke) = self.stroke {
294 g.stroke(Some(crate::scene::Stroke {
295 width: stroke.width,
296 color: stroke.color.with_alpha(stroke.color.a * opacity),
297 }));
298 }
299 match &self.shape {
300 VectorShape::Rect { rect } => {
301 g.draw_rect(*rect);
302 }
303 VectorShape::RoundedRect { rect, radius } => {
304 g.draw_rounded_rect(*rect, *radius);
305 }
306 VectorShape::Circle { center, radius } => {
307 g.draw_ellipse(*center, Vec2::splat(*radius));
308 }
309 VectorShape::Ellipse {
310 center,
311 half_extents,
312 } => {
313 g.draw_ellipse(*center, *half_extents);
314 }
315 VectorShape::Path { .. } => return None,
316 }
317 Some(g)
318 }
319
320 pub fn add_to_stage(
324 &self,
325 stage: &mut crate::scene::Stage,
326 parent: crate::scene::NodeId,
327 ) -> Option<crate::scene::NodeId> {
328 let g = self.to_graphics()?;
329 stage.add_child(parent, g)
330 }
331}
332
333fn scale_fill_alpha(fill: Fill, opacity: f32) -> Fill {
334 match fill {
335 Fill::Solid(c) => Fill::Solid(c.with_alpha(c.a * opacity)),
336 Fill::LinearGradient {
337 start,
338 end,
339 color_a,
340 color_b,
341 } => Fill::LinearGradient {
342 start,
343 end,
344 color_a: color_a.with_alpha(color_a.a * opacity),
345 color_b: color_b.with_alpha(color_b.a * opacity),
346 },
347 Fill::RadialGradient {
348 center,
349 radius,
350 color_a,
351 color_b,
352 } => Fill::RadialGradient {
353 center,
354 radius,
355 color_a: color_a.with_alpha(color_a.a * opacity),
356 color_b: color_b.with_alpha(color_b.a * opacity),
357 },
358 }
359}
360
361#[cfg(test)]
362mod tests {
363 use super::*;
364
365 #[test]
366 fn rect_bounds_match_rect() {
367 let r = Rect::new(-0.5, -0.3, 1.0, 0.6);
368 let bounds = VectorShape::rect(r).bounds();
369 assert!((bounds.min.x - r.min.x).abs() < 1e-6);
370 assert!((bounds.size.x - r.size.x).abs() < 1e-6);
371 }
372
373 #[test]
374 fn circle_bounds_are_2r_square() {
375 let bounds = VectorShape::circle(Vec2::new(0.1, -0.2), 0.3).bounds();
376 assert!((bounds.size.x - 0.6).abs() < 1e-6);
377 assert!((bounds.size.y - 0.6).abs() < 1e-6);
378 assert!((bounds.min.x - (-0.2)).abs() < 1e-6);
379 assert!((bounds.min.y - (-0.5)).abs() < 1e-6);
380 }
381
382 #[test]
383 fn ellipse_bounds_are_anisotropic() {
384 let bounds = VectorShape::ellipse(Vec2::ZERO, Vec2::new(0.7, 0.3)).bounds();
385 assert!((bounds.size.x - 1.4).abs() < 1e-6);
386 assert!((bounds.size.y - 0.6).abs() < 1e-6);
387 }
388
389 #[test]
390 fn path_bounds_min_max_extent() {
391 let pts = vec![
392 Vec2::new(-0.4, 0.1),
393 Vec2::new(0.2, -0.3),
394 Vec2::new(0.5, 0.6),
395 ];
396 let bounds = VectorShape::path(pts).bounds();
397 assert!((bounds.min.x - (-0.4)).abs() < 1e-6);
398 assert!((bounds.min.y - (-0.3)).abs() < 1e-6);
399 assert!((bounds.size.x - 0.9).abs() < 1e-6);
400 assert!((bounds.size.y - 0.9).abs() < 1e-6);
401 }
402
403 #[test]
404 fn empty_path_bounds_is_zero() {
405 let bounds = VectorShape::path(Vec::new()).bounds();
406 assert!(bounds.size.x.abs() < 1e-6);
407 assert!(bounds.size.y.abs() < 1e-6);
408 }
409
410 #[test]
411 fn analytic_shapes_round_trip_to_mask_shape() {
412 let r = Rect::new(-0.5, -0.5, 1.0, 1.0);
413 assert!(matches!(
414 VectorShape::rect(r).as_mask_shape(),
415 Some(MaskShape::Rect { .. })
416 ));
417 assert!(matches!(
418 VectorShape::rounded_rect(r, 0.2).as_mask_shape(),
419 Some(MaskShape::RoundedRect { .. })
420 ));
421 assert!(matches!(
422 VectorShape::circle(Vec2::ZERO, 0.4).as_mask_shape(),
423 Some(MaskShape::Circle { .. })
424 ));
425 assert!(matches!(
426 VectorShape::ellipse(Vec2::ZERO, Vec2::new(0.7, 0.3)).as_mask_shape(),
427 Some(MaskShape::Ellipse { .. })
428 ));
429 }
430
431 #[test]
432 fn path_does_not_round_trip_to_mask_shape() {
433 let path = VectorShape::path(vec![Vec2::ZERO, Vec2::new(0.5, 0.5)]);
434 assert!(path.as_mask_shape().is_none());
435 assert_eq!(path.as_path_points().map(<[_]>::len), Some(2));
436 }
437
438 #[test]
439 fn vector_default_opacity_is_one() {
440 let v = Vector::new(VectorShape::rect(Rect::new(-0.5, -0.5, 1.0, 1.0)));
441 assert!((v.opacity - 1.0).abs() < f32::EPSILON);
442 assert!(v.fill.is_none());
443 assert!(v.stroke.is_none());
444 }
445
446 #[test]
447 fn vector_builder_chains() {
448 let v = Vector::new(VectorShape::circle(Vec2::ZERO, 0.5))
449 .with_fill(Fill::Solid(Color::rgba(1.0, 0.5, 0.0, 1.0)))
450 .with_stroke(VectorStroke::new(0.02, Color::WHITE))
451 .with_opacity(0.8);
452 assert!(v.fill.is_some());
453 assert!(v.stroke.is_some());
454 assert!((v.opacity - 0.8).abs() < f32::EPSILON);
455 }
456}