1use crate::{MAX_TEXT_CONTENT_BYTES, MAX_TEXT_GENERATOR_FONT_SIZE, TextStyle, Vec2};
4use serde::{Deserialize, Serialize};
5
6pub const MAX_GRADIENT_STOPS: usize = 8;
8
9#[derive(Copy, Clone, Debug, Deserialize, PartialEq, Serialize)]
12#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
13#[serde(deny_unknown_fields)]
14pub struct Rgba {
15 pub red: f32,
17 pub green: f32,
19 pub blue: f32,
21 pub alpha: f32,
23}
24
25impl Rgba {
26 #[must_use]
28 pub const fn new(red: f32, green: f32, blue: f32, alpha: f32) -> Self {
29 Self {
30 red,
31 green,
32 blue,
33 alpha,
34 }
35 }
36
37 pub(crate) fn is_valid(self) -> bool {
38 [self.red, self.green, self.blue, self.alpha]
39 .iter()
40 .all(|value| (0.0..=1.0).contains(value))
41 }
42}
43
44#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
53#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
54#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
55pub enum Generator {
56 Fill {
58 paint: Paint,
60 },
61 Shape {
63 shape: GeneratedShape,
65 center: Vec2,
67 size: Vec2,
69 #[serde(default)]
71 rotation_degrees: f32,
72 #[serde(default)]
74 fill: Option<Paint>,
75 #[serde(default)]
77 stroke: Option<Stroke>,
78 },
79 Text {
84 text: String,
87 #[serde(default)]
90 style: TextStyle,
91 },
92}
93
94#[derive(Copy, Clone, Debug, Deserialize, PartialEq, Serialize)]
96#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
97#[serde(tag = "type", rename_all = "snake_case", from = "GeneratedShapeWire")]
98pub enum GeneratedShape {
99 Rectangle {
101 corner_radius: f32,
104 },
105 Ellipse,
107}
108
109#[derive(Deserialize)]
112#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
113#[cfg_attr(feature = "schema", schemars(rename = "GeneratedShape"))]
114#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
115pub(crate) enum GeneratedShapeWire {
116 Rectangle {
118 #[serde(default)]
121 corner_radius: f32,
122 },
123 Ellipse {},
125}
126
127impl From<GeneratedShapeWire> for GeneratedShape {
128 fn from(wire: GeneratedShapeWire) -> Self {
129 match wire {
130 GeneratedShapeWire::Rectangle { corner_radius } => Self::Rectangle { corner_radius },
131 GeneratedShapeWire::Ellipse {} => Self::Ellipse,
132 }
133 }
134}
135
136#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
138#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
139#[serde(deny_unknown_fields)]
140pub struct Stroke {
141 pub width: f32,
143 pub paint: Paint,
145}
146
147#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
154#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
155#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
156pub enum Paint {
157 Solid {
159 color: Rgba,
161 },
162 LinearGradient {
164 start: Vec2,
166 end: Vec2,
168 stops: Vec<GradientStop>,
170 #[serde(default)]
172 spread: Spread,
173 },
174 RadialGradient {
176 center: Vec2,
178 radius: f32,
181 stops: Vec<GradientStop>,
183 #[serde(default)]
185 spread: Spread,
186 },
187 ConicGradient {
189 center: Vec2,
191 #[serde(default)]
193 angle_degrees: f32,
194 stops: Vec<GradientStop>,
196 },
197}
198
199#[derive(Copy, Clone, Debug, Deserialize, PartialEq, Serialize)]
201#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
202#[serde(deny_unknown_fields)]
203pub struct GradientStop {
204 pub offset: f32,
206 pub color: Rgba,
208}
209
210#[derive(Copy, Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
212#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
213#[serde(rename_all = "snake_case")]
214pub enum Spread {
215 #[default]
217 Pad,
218 Repeat,
220 Reflect,
222}
223
224impl Generator {
225 pub(crate) fn is_valid(&self) -> bool {
226 match self {
227 Self::Fill { paint } => paint.is_valid(),
228 Self::Shape {
229 shape,
230 center,
231 size,
232 rotation_degrees,
233 fill,
234 stroke,
235 } => {
236 let shape_is_valid = match *shape {
237 GeneratedShape::Rectangle { corner_radius } => {
238 corner_radius.is_finite() && corner_radius >= 0.0
239 }
240 GeneratedShape::Ellipse => true,
241 };
242 shape_is_valid
243 && finite(*center)
244 && finite(*size)
245 && size.x >= 0.0
246 && size.y >= 0.0
247 && rotation_degrees.is_finite()
248 && (fill.is_some() || stroke.is_some())
249 && fill.as_ref().is_none_or(Paint::is_valid)
250 && stroke.as_ref().is_none_or(|stroke| {
251 stroke.width.is_finite() && stroke.width > 0.0 && stroke.paint.is_valid()
252 })
253 }
254 Self::Text { text, style } => {
255 !text.is_empty()
256 && text.len() <= MAX_TEXT_CONTENT_BYTES
257 && style.is_valid(MAX_TEXT_GENERATOR_FONT_SIZE)
258 }
259 }
260 }
261}
262
263impl Paint {
264 fn is_valid(&self) -> bool {
265 match self {
266 Self::Solid { color } => color.is_valid(),
267 Self::LinearGradient {
268 start, end, stops, ..
269 } => finite(*start) && finite(*end) && start != end && valid_stops(stops),
270 Self::RadialGradient {
271 center,
272 radius,
273 stops,
274 ..
275 } => finite(*center) && radius.is_finite() && *radius > 0.0 && valid_stops(stops),
276 Self::ConicGradient {
277 center,
278 angle_degrees,
279 stops,
280 } => finite(*center) && angle_degrees.is_finite() && valid_stops(stops),
281 }
282 }
283}
284
285fn finite(value: Vec2) -> bool {
286 value.x.is_finite() && value.y.is_finite()
287}
288
289fn valid_stops(stops: &[GradientStop]) -> bool {
290 (2..=MAX_GRADIENT_STOPS).contains(&stops.len())
291 && stops
292 .iter()
293 .all(|stop| (0.0..=1.0).contains(&stop.offset) && stop.color.is_valid())
294 && stops
295 .windows(2)
296 .all(|pair| pair[0].offset <= pair[1].offset)
297}