use crate::{MAX_TEXT_CONTENT_BYTES, MAX_TEXT_GENERATOR_FONT_SIZE, TextStyle, Vec2};
use serde::{Deserialize, Serialize};
pub const MAX_GRADIENT_STOPS: usize = 8;
#[derive(Copy, Clone, Debug, Deserialize, PartialEq, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct Rgba {
pub red: f32,
pub green: f32,
pub blue: f32,
pub alpha: f32,
}
impl Rgba {
#[must_use]
pub const fn new(red: f32, green: f32, blue: f32, alpha: f32) -> Self {
Self {
red,
green,
blue,
alpha,
}
}
pub(crate) fn is_valid(self) -> bool {
[self.red, self.green, self.blue, self.alpha]
.iter()
.all(|value| (0.0..=1.0).contains(value))
}
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
pub enum Generator {
Fill {
paint: Paint,
},
Shape {
shape: GeneratedShape,
center: Vec2,
size: Vec2,
#[serde(default)]
rotation_degrees: f32,
#[serde(default)]
fill: Option<Paint>,
#[serde(default)]
stroke: Option<Stroke>,
},
Text {
text: String,
#[serde(default)]
style: TextStyle,
},
}
#[derive(Copy, Clone, Debug, Deserialize, PartialEq, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(tag = "type", rename_all = "snake_case", from = "GeneratedShapeWire")]
pub enum GeneratedShape {
Rectangle {
corner_radius: f32,
},
Ellipse,
}
#[derive(Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[cfg_attr(feature = "schema", schemars(rename = "GeneratedShape"))]
#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
pub(crate) enum GeneratedShapeWire {
Rectangle {
#[serde(default)]
corner_radius: f32,
},
Ellipse {},
}
impl From<GeneratedShapeWire> for GeneratedShape {
fn from(wire: GeneratedShapeWire) -> Self {
match wire {
GeneratedShapeWire::Rectangle { corner_radius } => Self::Rectangle { corner_radius },
GeneratedShapeWire::Ellipse {} => Self::Ellipse,
}
}
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct Stroke {
pub width: f32,
pub paint: Paint,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
pub enum Paint {
Solid {
color: Rgba,
},
LinearGradient {
start: Vec2,
end: Vec2,
stops: Vec<GradientStop>,
#[serde(default)]
spread: Spread,
},
RadialGradient {
center: Vec2,
radius: f32,
stops: Vec<GradientStop>,
#[serde(default)]
spread: Spread,
},
ConicGradient {
center: Vec2,
#[serde(default)]
angle_degrees: f32,
stops: Vec<GradientStop>,
},
}
#[derive(Copy, Clone, Debug, Deserialize, PartialEq, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct GradientStop {
pub offset: f32,
pub color: Rgba,
}
#[derive(Copy, Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "snake_case")]
pub enum Spread {
#[default]
Pad,
Repeat,
Reflect,
}
impl Generator {
#[must_use]
pub fn is_valid(&self) -> bool {
match self {
Self::Fill { paint } => paint.is_valid(),
Self::Shape {
shape,
center,
size,
rotation_degrees,
fill,
stroke,
} => {
let shape_is_valid = match *shape {
GeneratedShape::Rectangle { corner_radius } => {
corner_radius.is_finite() && corner_radius >= 0.0
}
GeneratedShape::Ellipse => true,
};
shape_is_valid
&& finite(*center)
&& finite(*size)
&& size.x >= 0.0
&& size.y >= 0.0
&& rotation_degrees.is_finite()
&& (fill.is_some() || stroke.is_some())
&& fill.as_ref().is_none_or(Paint::is_valid)
&& stroke.as_ref().is_none_or(|stroke| {
stroke.width.is_finite() && stroke.width > 0.0 && stroke.paint.is_valid()
})
}
Self::Text { text, style } => {
!text.is_empty()
&& text.len() <= MAX_TEXT_CONTENT_BYTES
&& style.is_valid(MAX_TEXT_GENERATOR_FONT_SIZE)
}
}
}
}
impl Paint {
fn is_valid(&self) -> bool {
match self {
Self::Solid { color } => color.is_valid(),
Self::LinearGradient {
start, end, stops, ..
} => finite(*start) && finite(*end) && start != end && valid_stops(stops),
Self::RadialGradient {
center,
radius,
stops,
..
} => finite(*center) && radius.is_finite() && *radius > 0.0 && valid_stops(stops),
Self::ConicGradient {
center,
angle_degrees,
stops,
} => finite(*center) && angle_degrees.is_finite() && valid_stops(stops),
}
}
}
fn finite(value: Vec2) -> bool {
value.x.is_finite() && value.y.is_finite()
}
fn valid_stops(stops: &[GradientStop]) -> bool {
(2..=MAX_GRADIENT_STOPS).contains(&stops.len())
&& stops
.iter()
.all(|stop| (0.0..=1.0).contains(&stop.offset) && stop.color.is_valid())
&& stops
.windows(2)
.all(|pair| pair[0].offset <= pair[1].offset)
}