astrelis_geometry/
stroke.rs1use crate::Paint;
6use astrelis_render::Color;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
10pub enum LineCap {
11 #[default]
13 Butt,
14 Round,
16 Square,
18}
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
22pub enum LineJoin {
23 #[default]
25 Miter,
26 Round,
28 Bevel,
30}
31
32#[derive(Debug, Clone, PartialEq)]
34pub struct DashPattern {
35 pub pattern: Vec<f32>,
37 pub offset: f32,
39}
40
41impl DashPattern {
42 pub fn new(pattern: Vec<f32>, offset: f32) -> Self {
44 Self { pattern, offset }
45 }
46
47 pub fn dashed(dash: f32, gap: f32) -> Self {
49 Self {
50 pattern: vec![dash, gap],
51 offset: 0.0,
52 }
53 }
54
55 pub fn dotted(gap: f32) -> Self {
57 Self {
58 pattern: vec![0.0, gap],
59 offset: 0.0,
60 }
61 }
62
63 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#[derive(Debug, Clone, PartialEq)]
74pub struct Stroke {
75 pub width: f32,
77 pub paint: Paint,
79 pub line_cap: LineCap,
81 pub line_join: LineJoin,
83 pub miter_limit: f32,
85 pub dash: Option<DashPattern>,
87 pub opacity: f32,
89}
90
91impl Stroke {
92 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 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 pub fn with_line_cap(mut self, cap: LineCap) -> Self {
120 self.line_cap = cap;
121 self
122 }
123
124 pub fn with_line_join(mut self, join: LineJoin) -> Self {
126 self.line_join = join;
127 self
128 }
129
130 pub fn with_miter_limit(mut self, limit: f32) -> Self {
132 self.miter_limit = limit.max(1.0);
133 self
134 }
135
136 pub fn with_dash(mut self, pattern: DashPattern) -> Self {
138 self.dash = Some(pattern);
139 self
140 }
141
142 pub fn dashed(mut self, dash: f32, gap: f32) -> Self {
144 self.dash = Some(DashPattern::dashed(dash, gap));
145 self
146 }
147
148 pub fn with_opacity(mut self, opacity: f32) -> Self {
150 self.opacity = opacity.clamp(0.0, 1.0);
151 self
152 }
153
154 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 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}