Skip to main content

astrelis_geometry/
shape.rs

1//! High-level shape primitives.
2//!
3//! Shapes are convenient wrappers around paths for common geometric forms.
4
5use crate::{Path, PathBuilder};
6use glam::Vec2;
7
8/// A high-level shape that can be converted to a path.
9#[derive(Debug, Clone, PartialEq)]
10pub enum Shape {
11    /// A rectangle.
12    Rect {
13        /// Top-left position
14        position: Vec2,
15        /// Size (width, height)
16        size: Vec2,
17    },
18    /// A rounded rectangle.
19    RoundedRect {
20        /// Top-left position
21        position: Vec2,
22        /// Size (width, height)
23        size: Vec2,
24        /// Corner radii [top-left, top-right, bottom-right, bottom-left]
25        radii: [f32; 4],
26    },
27    /// A circle.
28    Circle {
29        /// Center point
30        center: Vec2,
31        /// Radius
32        radius: f32,
33    },
34    /// An ellipse.
35    Ellipse {
36        /// Center point
37        center: Vec2,
38        /// Radii (x, y)
39        radii: Vec2,
40    },
41    /// A line segment.
42    Line {
43        /// Start point
44        start: Vec2,
45        /// End point
46        end: Vec2,
47    },
48    /// A polyline (connected line segments).
49    Polyline {
50        /// Points defining the polyline
51        points: Vec<Vec2>,
52        /// Whether to close the polyline into a polygon
53        closed: bool,
54    },
55    /// A polygon (closed polyline).
56    Polygon {
57        /// Vertices of the polygon
58        points: Vec<Vec2>,
59    },
60    /// A regular polygon with n sides.
61    RegularPolygon {
62        /// Center point
63        center: Vec2,
64        /// Radius (distance from center to vertices)
65        radius: f32,
66        /// Number of sides
67        sides: u32,
68        /// Rotation offset in radians
69        rotation: f32,
70    },
71    /// A star shape.
72    Star {
73        /// Center point
74        center: Vec2,
75        /// Outer radius (tips)
76        outer_radius: f32,
77        /// Inner radius (valleys)
78        inner_radius: f32,
79        /// Number of points
80        points: u32,
81        /// Rotation offset in radians
82        rotation: f32,
83    },
84    /// An arc (partial circle).
85    Arc {
86        /// Center point
87        center: Vec2,
88        /// Radius
89        radius: f32,
90        /// Start angle in radians
91        start_angle: f32,
92        /// End angle in radians
93        end_angle: f32,
94    },
95    /// A pie/sector (arc with lines to center).
96    Pie {
97        /// Center point
98        center: Vec2,
99        /// Radius
100        radius: f32,
101        /// Start angle in radians
102        start_angle: f32,
103        /// End angle in radians
104        end_angle: f32,
105    },
106    /// A custom path.
107    Path(Path),
108}
109
110impl Shape {
111    // =========================================================================
112    // Constructors
113    // =========================================================================
114
115    /// Create a rectangle.
116    pub fn rect(position: Vec2, size: Vec2) -> Self {
117        Self::Rect { position, size }
118    }
119
120    /// Create a rectangle from center and size.
121    pub fn rect_centered(center: Vec2, size: Vec2) -> Self {
122        Self::Rect {
123            position: center - size * 0.5,
124            size,
125        }
126    }
127
128    /// Create a rounded rectangle with uniform corner radius.
129    pub fn rounded_rect(position: Vec2, size: Vec2, radius: f32) -> Self {
130        Self::RoundedRect {
131            position,
132            size,
133            radii: [radius; 4],
134        }
135    }
136
137    /// Create a rounded rectangle with individual corner radii.
138    pub fn rounded_rect_varying(position: Vec2, size: Vec2, radii: [f32; 4]) -> Self {
139        Self::RoundedRect {
140            position,
141            size,
142            radii,
143        }
144    }
145
146    /// Create a circle.
147    pub fn circle(center: Vec2, radius: f32) -> Self {
148        Self::Circle { center, radius }
149    }
150
151    /// Create an ellipse.
152    pub fn ellipse(center: Vec2, radii: Vec2) -> Self {
153        Self::Ellipse { center, radii }
154    }
155
156    /// Create a line segment.
157    pub fn line(start: Vec2, end: Vec2) -> Self {
158        Self::Line { start, end }
159    }
160
161    /// Create a polyline.
162    pub fn polyline(points: Vec<Vec2>, closed: bool) -> Self {
163        Self::Polyline { points, closed }
164    }
165
166    /// Create a polygon.
167    pub fn polygon(points: Vec<Vec2>) -> Self {
168        Self::Polygon { points }
169    }
170
171    /// Create a regular polygon.
172    pub fn regular_polygon(center: Vec2, radius: f32, sides: u32) -> Self {
173        Self::RegularPolygon {
174            center,
175            radius,
176            sides,
177            rotation: 0.0,
178        }
179    }
180
181    /// Create a regular polygon with rotation.
182    pub fn regular_polygon_rotated(center: Vec2, radius: f32, sides: u32, rotation: f32) -> Self {
183        Self::RegularPolygon {
184            center,
185            radius,
186            sides,
187            rotation,
188        }
189    }
190
191    /// Create a star.
192    pub fn star(center: Vec2, outer_radius: f32, inner_radius: f32, points: u32) -> Self {
193        Self::Star {
194            center,
195            outer_radius,
196            inner_radius,
197            points,
198            rotation: 0.0,
199        }
200    }
201
202    /// Create a star with rotation.
203    pub fn star_rotated(
204        center: Vec2,
205        outer_radius: f32,
206        inner_radius: f32,
207        points: u32,
208        rotation: f32,
209    ) -> Self {
210        Self::Star {
211            center,
212            outer_radius,
213            inner_radius,
214            points,
215            rotation,
216        }
217    }
218
219    /// Create an arc.
220    pub fn arc(center: Vec2, radius: f32, start_angle: f32, end_angle: f32) -> Self {
221        Self::Arc {
222            center,
223            radius,
224            start_angle,
225            end_angle,
226        }
227    }
228
229    /// Create a pie/sector.
230    pub fn pie(center: Vec2, radius: f32, start_angle: f32, end_angle: f32) -> Self {
231        Self::Pie {
232            center,
233            radius,
234            start_angle,
235            end_angle,
236        }
237    }
238
239    /// Create from a path.
240    pub fn path(path: Path) -> Self {
241        Self::Path(path)
242    }
243
244    // =========================================================================
245    // Conversion
246    // =========================================================================
247
248    /// Convert this shape to a path.
249    pub fn to_path(&self) -> Path {
250        let mut builder = PathBuilder::new();
251
252        match self {
253            Shape::Rect { position, size } => {
254                builder.rect(*position, *size);
255            }
256
257            Shape::RoundedRect {
258                position,
259                size,
260                radii,
261            } => {
262                // Use the first radius for uniform (simplified)
263                // TODO: Support varying radii per corner
264                let r = radii[0].min(size.x / 2.0).min(size.y / 2.0);
265                builder.rounded_rect(*position, *size, r);
266            }
267
268            Shape::Circle { center, radius } => {
269                builder.circle(*center, *radius);
270            }
271
272            Shape::Ellipse { center, radii } => {
273                builder.ellipse(*center, *radii);
274            }
275
276            Shape::Line { start, end } => {
277                builder.move_to(*start);
278                builder.line_to(*end);
279            }
280
281            Shape::Polyline { points, closed } => {
282                if !points.is_empty() {
283                    builder.move_to(points[0]);
284                    for point in &points[1..] {
285                        builder.line_to(*point);
286                    }
287                    if *closed {
288                        builder.close();
289                    }
290                }
291            }
292
293            Shape::Polygon { points } => {
294                builder.polygon(points);
295            }
296
297            Shape::RegularPolygon {
298                center,
299                radius,
300                sides,
301                rotation,
302            } => {
303                let points = generate_regular_polygon(*center, *radius, *sides, *rotation);
304                builder.polygon(&points);
305            }
306
307            Shape::Star {
308                center,
309                outer_radius,
310                inner_radius,
311                points,
312                rotation,
313            } => {
314                let star_points =
315                    generate_star(*center, *outer_radius, *inner_radius, *points, *rotation);
316                builder.polygon(&star_points);
317            }
318
319            Shape::Arc {
320                center,
321                radius,
322                start_angle,
323                end_angle,
324            } => {
325                let arc_points = approximate_arc(*center, *radius, *start_angle, *end_angle, 32);
326                if !arc_points.is_empty() {
327                    builder.move_to(arc_points[0]);
328                    for point in &arc_points[1..] {
329                        builder.line_to(*point);
330                    }
331                }
332            }
333
334            Shape::Pie {
335                center,
336                radius,
337                start_angle,
338                end_angle,
339            } => {
340                let arc_points = approximate_arc(*center, *radius, *start_angle, *end_angle, 32);
341                builder.move_to(*center);
342                if !arc_points.is_empty() {
343                    builder.line_to(arc_points[0]);
344                    for point in &arc_points[1..] {
345                        builder.line_to(*point);
346                    }
347                }
348                builder.close();
349            }
350
351            Shape::Path(path) => {
352                return path.clone();
353            }
354        }
355
356        builder.build()
357    }
358
359    /// Get the bounding box of this shape.
360    pub fn bounds(&self) -> Option<(Vec2, Vec2)> {
361        match self {
362            Shape::Rect { position, size } => Some((*position, *position + *size)),
363
364            Shape::RoundedRect { position, size, .. } => Some((*position, *position + *size)),
365
366            Shape::Circle { center, radius } => {
367                let r = Vec2::splat(*radius);
368                Some((*center - r, *center + r))
369            }
370
371            Shape::Ellipse { center, radii } => Some((*center - *radii, *center + *radii)),
372
373            Shape::Line { start, end } => Some((start.min(*end), start.max(*end))),
374
375            Shape::Polyline { points, .. } | Shape::Polygon { points } => {
376                if points.is_empty() {
377                    return None;
378                }
379                let mut min = points[0];
380                let mut max = points[0];
381                for p in &points[1..] {
382                    min = min.min(*p);
383                    max = max.max(*p);
384                }
385                Some((min, max))
386            }
387
388            Shape::RegularPolygon { center, radius, .. } => {
389                let r = Vec2::splat(*radius);
390                Some((*center - r, *center + r))
391            }
392
393            Shape::Star {
394                center,
395                outer_radius,
396                ..
397            } => {
398                let r = Vec2::splat(*outer_radius);
399                Some((*center - r, *center + r))
400            }
401
402            Shape::Arc { center, radius, .. } | Shape::Pie { center, radius, .. } => {
403                // Conservative bounds
404                let r = Vec2::splat(*radius);
405                Some((*center - r, *center + r))
406            }
407
408            Shape::Path(path) => path.bounds(),
409        }
410    }
411}
412
413/// Generate vertices for a regular polygon.
414fn generate_regular_polygon(center: Vec2, radius: f32, sides: u32, rotation: f32) -> Vec<Vec2> {
415    let mut points = Vec::with_capacity(sides as usize);
416    let angle_step = std::f32::consts::TAU / sides as f32;
417
418    for i in 0..sides {
419        let angle = rotation + angle_step * i as f32;
420        points.push(center + Vec2::new(angle.cos(), angle.sin()) * radius);
421    }
422
423    points
424}
425
426/// Generate vertices for a star.
427fn generate_star(
428    center: Vec2,
429    outer_radius: f32,
430    inner_radius: f32,
431    points: u32,
432    rotation: f32,
433) -> Vec<Vec2> {
434    let mut vertices = Vec::with_capacity(points as usize * 2);
435    let angle_step = std::f32::consts::TAU / (points * 2) as f32;
436
437    for i in 0..(points * 2) {
438        let angle = rotation - std::f32::consts::FRAC_PI_2 + angle_step * i as f32;
439        let radius = if i % 2 == 0 {
440            outer_radius
441        } else {
442            inner_radius
443        };
444        vertices.push(center + Vec2::new(angle.cos(), angle.sin()) * radius);
445    }
446
447    vertices
448}
449
450/// Approximate an arc with line segments.
451fn approximate_arc(
452    center: Vec2,
453    radius: f32,
454    start_angle: f32,
455    end_angle: f32,
456    segments: u32,
457) -> Vec<Vec2> {
458    let mut points = Vec::with_capacity(segments as usize + 1);
459    let angle_span = end_angle - start_angle;
460    let angle_step = angle_span / segments as f32;
461
462    for i in 0..=segments {
463        let angle = start_angle + angle_step * i as f32;
464        points.push(center + Vec2::new(angle.cos(), angle.sin()) * radius);
465    }
466
467    points
468}
469
470#[cfg(test)]
471mod tests {
472    use super::*;
473
474    #[test]
475    fn test_rect_bounds() {
476        let shape = Shape::rect(Vec2::new(10.0, 20.0), Vec2::new(100.0, 50.0));
477        let (min, max) = shape.bounds().unwrap();
478        assert_eq!(min, Vec2::new(10.0, 20.0));
479        assert_eq!(max, Vec2::new(110.0, 70.0));
480    }
481
482    #[test]
483    fn test_circle_bounds() {
484        let shape = Shape::circle(Vec2::new(50.0, 50.0), 25.0);
485        let (min, max) = shape.bounds().unwrap();
486        assert_eq!(min, Vec2::new(25.0, 25.0));
487        assert_eq!(max, Vec2::new(75.0, 75.0));
488    }
489
490    #[test]
491    fn test_rect_to_path() {
492        let shape = Shape::rect(Vec2::new(0.0, 0.0), Vec2::new(100.0, 100.0));
493        let path = shape.to_path();
494        assert!(!path.is_empty());
495    }
496
497    #[test]
498    fn test_regular_polygon() {
499        let points = generate_regular_polygon(Vec2::ZERO, 10.0, 4, 0.0);
500        assert_eq!(points.len(), 4);
501    }
502
503    #[test]
504    fn test_star() {
505        let points = generate_star(Vec2::ZERO, 10.0, 5.0, 5, 0.0);
506        assert_eq!(points.len(), 10);
507    }
508}