Skip to main content

astrelis_geometry/
stroke.rs

1//! Stroke properties for geometry outlines.
2//!
3//! Defines how paths are stroked: width, caps, joins, and dash patterns.
4
5use crate::Paint;
6use astrelis_render::Color;
7
8/// Line cap style for stroke endpoints.
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
10pub enum LineCap {
11    /// Flat cap ending at the endpoint.
12    #[default]
13    Butt,
14    /// Round cap extending beyond the endpoint.
15    Round,
16    /// Square cap extending beyond the endpoint.
17    Square,
18}
19
20/// Line join style for stroke corners.
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
22pub enum LineJoin {
23    /// Miter join (sharp corner).
24    #[default]
25    Miter,
26    /// Round join (rounded corner).
27    Round,
28    /// Bevel join (flat corner).
29    Bevel,
30}
31
32/// Dash pattern for stroked lines.
33#[derive(Debug, Clone, PartialEq)]
34pub struct DashPattern {
35    /// Alternating on/off lengths.
36    pub pattern: Vec<f32>,
37    /// Offset into the pattern to start.
38    pub offset: f32,
39}
40
41impl DashPattern {
42    /// Create a new dash pattern.
43    pub fn new(pattern: Vec<f32>, offset: f32) -> Self {
44        Self { pattern, offset }
45    }
46
47    /// Create a simple dashed line.
48    pub fn dashed(dash: f32, gap: f32) -> Self {
49        Self {
50            pattern: vec![dash, gap],
51            offset: 0.0,
52        }
53    }
54
55    /// Create a dotted line.
56    pub fn dotted(gap: f32) -> Self {
57        Self {
58            pattern: vec![0.0, gap],
59            offset: 0.0,
60        }
61    }
62
63    /// Create a dash-dot pattern.
64    pub fn dash_dot(dash: f32, gap: f32, dot: f32) -> Self {
65        Self {
66            pattern: vec![dash, gap, dot, gap],
67            offset: 0.0,
68        }
69    }
70}
71
72/// Stroke properties for geometry outlines.
73#[derive(Debug, Clone, PartialEq)]
74pub struct Stroke {
75    /// Stroke width in logical pixels
76    pub width: f32,
77    /// Paint for the stroke color/gradient
78    pub paint: Paint,
79    /// Line cap style
80    pub line_cap: LineCap,
81    /// Line join style
82    pub line_join: LineJoin,
83    /// Miter limit for miter joins
84    pub miter_limit: f32,
85    /// Optional dash pattern
86    pub dash: Option<DashPattern>,
87    /// Opacity multiplier (0.0 to 1.0)
88    pub opacity: f32,
89}
90
91impl Stroke {
92    /// Create a solid color stroke.
93    pub fn solid(color: Color, width: f32) -> Self {
94        Self {
95            width,
96            paint: Paint::Solid(color),
97            line_cap: LineCap::Butt,
98            line_join: LineJoin::Miter,
99            miter_limit: 4.0,
100            dash: None,
101            opacity: 1.0,
102        }
103    }
104
105    /// Create a stroke from a paint.
106    pub fn from_paint(paint: Paint, width: f32) -> Self {
107        Self {
108            width,
109            paint,
110            line_cap: LineCap::Butt,
111            line_join: LineJoin::Miter,
112            miter_limit: 4.0,
113            dash: None,
114            opacity: 1.0,
115        }
116    }
117
118    /// Set the line cap style.
119    pub fn with_line_cap(mut self, cap: LineCap) -> Self {
120        self.line_cap = cap;
121        self
122    }
123
124    /// Set the line join style.
125    pub fn with_line_join(mut self, join: LineJoin) -> Self {
126        self.line_join = join;
127        self
128    }
129
130    /// Set the miter limit.
131    pub fn with_miter_limit(mut self, limit: f32) -> Self {
132        self.miter_limit = limit.max(1.0);
133        self
134    }
135
136    /// Set a dash pattern.
137    pub fn with_dash(mut self, pattern: DashPattern) -> Self {
138        self.dash = Some(pattern);
139        self
140    }
141
142    /// Set a simple dashed pattern.
143    pub fn dashed(mut self, dash: f32, gap: f32) -> Self {
144        self.dash = Some(DashPattern::dashed(dash, gap));
145        self
146    }
147
148    /// Set the opacity.
149    pub fn with_opacity(mut self, opacity: f32) -> Self {
150        self.opacity = opacity.clamp(0.0, 1.0);
151        self
152    }
153
154    /// Get the effective color (for solid strokes).
155    pub fn effective_color(&self) -> Option<Color> {
156        match &self.paint {
157            Paint::Solid(color) => Some(Color::rgba(
158                color.r,
159                color.g,
160                color.b,
161                color.a * self.opacity,
162            )),
163            _ => None,
164        }
165    }
166
167    /// Check if the stroke is visible.
168    pub fn is_visible(&self) -> bool {
169        self.width > 0.0 && self.opacity > 0.0
170    }
171}
172
173impl Default for Stroke {
174    fn default() -> Self {
175        Self::solid(Color::BLACK, 1.0)
176    }
177}
178
179#[cfg(test)]
180mod tests {
181    use super::*;
182
183    #[test]
184    fn test_solid_stroke() {
185        let stroke = Stroke::solid(Color::RED, 2.0);
186        assert_eq!(stroke.width, 2.0);
187        assert!(stroke.is_visible());
188    }
189
190    #[test]
191    fn test_dashed_stroke() {
192        let stroke = Stroke::solid(Color::BLUE, 1.0).dashed(5.0, 3.0);
193        assert!(stroke.dash.is_some());
194    }
195
196    #[test]
197    fn test_stroke_visibility() {
198        let stroke = Stroke::solid(Color::RED, 0.0);
199        assert!(!stroke.is_visible());
200
201        let stroke = Stroke::solid(Color::RED, 1.0).with_opacity(0.0);
202        assert!(!stroke.is_visible());
203    }
204}