1use std::borrow::Cow;
4
5use schemars::{JsonSchema, Schema, SchemaGenerator, json_schema};
6use serde::{Deserialize, Deserializer, Serialize, Serializer};
7
8use crate::error::IrError;
9
10#[derive(Debug, Clone, Copy, PartialEq)]
16pub struct Color {
17 pub r: f32,
19 pub g: f32,
21 pub b: f32,
23 pub a: f32,
25}
26
27impl Color {
28 pub const BLACK: Color = Color::rgb(0.0, 0.0, 0.0);
30 pub const WHITE: Color = Color::rgb(1.0, 1.0, 1.0);
32
33 pub const fn rgb(r: f32, g: f32, b: f32) -> Self {
35 Self { r, g, b, a: 1.0 }
36 }
37
38 pub const fn rgba(r: f32, g: f32, b: f32, a: f32) -> Self {
40 Self { r, g, b, a }
41 }
42
43 pub fn from_hex(hex: &str) -> Result<Self, IrError> {
53 let invalid = || IrError::InvalidColor(hex.to_owned());
54 let digits = hex.strip_prefix('#').ok_or_else(invalid)?.as_bytes();
55 if !matches!(digits.len(), 6 | 8) || !digits.iter().all(u8::is_ascii_hexdigit) {
56 return Err(invalid());
57 }
58 let component = |i: usize| {
59 let byte = (hex_value(digits[i]) << 4) | hex_value(digits[i + 1]);
60 f32::from(byte) / 255.0
61 };
62 let a = if digits.len() == 8 { component(6) } else { 1.0 };
63 Ok(Self::rgba(component(0), component(2), component(4), a))
64 }
65
66 pub fn to_hex(&self) -> String {
69 let [r, g, b, a] = [self.r, self.g, self.b, self.a].map(level);
70 if a == u8::MAX {
71 format!("#{r:02x}{g:02x}{b:02x}")
72 } else {
73 format!("#{r:02x}{g:02x}{b:02x}{a:02x}")
74 }
75 }
76}
77
78fn hex_value(digit: u8) -> u8 {
80 match digit {
81 b'0'..=b'9' => digit - b'0',
82 b'a'..=b'f' => digit - b'a' + 10,
83 _ => digit - b'A' + 10,
84 }
85}
86
87fn level(component: f32) -> u8 {
90 (component.clamp(0.0, 1.0) * 255.0).round() as u8
92}
93
94impl Default for Color {
95 fn default() -> Self {
96 Self::BLACK
97 }
98}
99
100impl Serialize for Color {
101 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
102 serializer.serialize_str(&self.to_hex())
103 }
104}
105
106impl<'de> Deserialize<'de> for Color {
107 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
108 let hex = String::deserialize(deserializer)?;
109 Color::from_hex(&hex).map_err(serde::de::Error::custom)
110 }
111}
112
113impl JsonSchema for Color {
114 fn schema_name() -> Cow<'static, str> {
115 "Color".into()
116 }
117
118 fn json_schema(_generator: &mut SchemaGenerator) -> Schema {
119 json_schema!({
120 "type": "string",
121 "pattern": "^#([0-9a-fA-F]{6}|[0-9a-fA-F]{8})$",
122 "description": "An sRGB colour with straight alpha, written as #rrggbb when opaque or #rrggbbaa otherwise."
123 })
124 }
125}
126
127#[derive(Debug, Clone, Copy, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
129#[serde(tag = "type", rename_all = "snake_case")]
130pub enum ColorSpec {
131 #[default]
135 Auto,
136 Rgba {
138 color: Color,
140 },
141 None,
143 Colormapped,
146}
147
148#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, JsonSchema)]
150pub struct LineStyle {
151 pub color: ColorSpec,
153 pub width_pt: f64,
155 pub dash: DashStyle,
157}
158
159impl Default for LineStyle {
160 fn default() -> Self {
161 Self {
162 color: ColorSpec::Auto,
163 width_pt: 0.75,
164 dash: DashStyle::Solid,
165 }
166 }
167}
168
169#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
171#[serde(rename_all = "snake_case")]
172pub enum DashStyle {
173 #[default]
175 Solid,
176 Dashed,
178 Dotted,
180 DashDot,
182 None,
184}
185
186#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, JsonSchema)]
188pub struct MarkerStyle {
189 pub shape: MarkerShape,
191 pub size_pt: f64,
193 pub face: ColorSpec,
195 pub edge: ColorSpec,
197}
198
199impl Default for MarkerStyle {
200 fn default() -> Self {
201 Self {
202 shape: MarkerShape::None,
203 size_pt: 4.0,
204 face: ColorSpec::None,
205 edge: ColorSpec::Auto,
206 }
207 }
208}
209
210#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
212#[serde(rename_all = "snake_case")]
213pub enum MarkerShape {
214 #[default]
216 None,
217 Circle,
219 Square,
221 Diamond,
223 TriangleUp,
225 TriangleDown,
227 Plus,
229 Cross,
231 Point,
233}