Skip to main content

img_gen_spec/validators/layers/
polygon.rs

1use super::{Border, ColorKind, LayerOffset};
2
3#[cfg(feature = "pyo3")]
4use pyo3::prelude::*;
5
6use serde::{Deserialize, Serialize};
7
8/// A custom type to ensure regular polygons have at least 3 sides.
9#[derive(Debug, Clone, Copy, Serialize)]
10#[cfg_attr(feature = "pyo3", pyclass(module = "img_gen", from_py_object))]
11pub struct RegularPolygonSides(u32);
12
13impl RegularPolygonSides {
14    /// Returns [`None`] when `sides < 3`.
15    pub fn new(sides: u32) -> Option<Self> {
16        if sides < 3 { None } else { Some(Self(sides)) }
17    }
18
19    /// Returns the validated number of polygon sides.
20    pub fn get(&self) -> u32 {
21        self.0
22    }
23}
24
25/// A custom type to ensure irregular polygons have at least 3 offsets.
26#[derive(Debug, Clone, Serialize)]
27#[cfg_attr(feature = "pyo3", pyclass(module = "img_gen", from_py_object))]
28pub struct IrregularPolygonSides(Vec<LayerOffset>);
29
30impl IrregularPolygonSides {
31    /// Returns [`None`] when `offsets.len() < 3`.
32    pub fn new(offsets: Vec<LayerOffset>) -> Option<Self> {
33        let mut unique = Vec::new();
34        for offset in offsets {
35            if !unique.contains(&offset) {
36                unique.push(offset);
37            }
38        }
39        if unique.len() < 3 {
40            None
41        } else {
42            Some(Self(unique))
43        }
44    }
45
46    /// Returns the unique polygon points as a slice.
47    pub fn as_slice(&self) -> &[LayerOffset] {
48        &self.0
49    }
50}
51
52/// A custom type to ensure polygon data has a minimum of 3 points.
53#[cfg_attr(feature = "pyo3", pyclass(module = "img_gen", from_py_object))]
54#[derive(Debug, Clone)]
55pub enum PolygonSides {
56    /// A polygon described by a validated side count.
57    Regular(RegularPolygonSides),
58    /// A polygon described by explicit vertex offsets.
59    Irregular(IrregularPolygonSides),
60}
61
62impl From<RegularPolygonSides> for PolygonSides {
63    fn from(value: RegularPolygonSides) -> Self {
64        Self::Regular(value)
65    }
66}
67
68impl From<IrregularPolygonSides> for PolygonSides {
69    fn from(value: IrregularPolygonSides) -> Self {
70        Self::Irregular(value)
71    }
72}
73
74impl Default for PolygonSides {
75    fn default() -> Self {
76        RegularPolygonSides(3).into()
77    }
78}
79
80impl<'de> Deserialize<'de> for RegularPolygonSides {
81    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
82    where
83        D: serde::Deserializer<'de>,
84    {
85        use serde::de::Error;
86
87        let sides = u32::deserialize(deserializer)?;
88        RegularPolygonSides::new(sides).ok_or_else(|| {
89            D::Error::custom(format!(
90                "Regular Polygons cannot have less than 3 sides, got {sides}"
91            ))
92        })
93    }
94}
95
96impl<'de> Deserialize<'de> for IrregularPolygonSides {
97    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
98    where
99        D: serde::Deserializer<'de>,
100    {
101        use serde::de::Error;
102
103        let offsets = Vec::<LayerOffset>::deserialize(deserializer)?;
104        IrregularPolygonSides::new(offsets).ok_or_else(|| {
105            D::Error::custom("Irregular Polygons cannot have less than 3 unique points")
106        })
107    }
108}
109
110impl<'de> Deserialize<'de> for PolygonSides {
111    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
112    where
113        D: serde::Deserializer<'de>,
114    {
115        PolygonSides::deserialize_sides(deserializer)
116    }
117}
118
119impl Serialize for PolygonSides {
120    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
121    where
122        S: serde::Serializer,
123    {
124        PolygonSides::serialize_sides(self, serializer)
125    }
126}
127
128impl PolygonSides {
129    /// Deserializes either a regular side count or a list of irregular polygon points.
130    ///
131    /// Integer values produce [`PolygonSides::Regular`], while sequences of unique offsets
132    /// produce [`PolygonSides::Irregular`].
133    pub fn deserialize<'de, D>(deserializer: D) -> Result<Self, D::Error>
134    where
135        D: serde::Deserializer<'de>,
136    {
137        PolygonSides::deserialize_sides(deserializer)
138    }
139
140    fn deserialize_sides<'de, D>(deserializer: D) -> Result<Self, D::Error>
141    where
142        D: serde::Deserializer<'de>,
143    {
144        use serde::de::{Error, SeqAccess, Visitor};
145
146        struct PolygonSidesVisitor;
147
148        impl<'de> Visitor<'de> for PolygonSidesVisitor {
149            type Value = PolygonSides;
150
151            fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
152                formatter.write_str("an integer >= 3 or a sequence of at least 3 unique offsets")
153            }
154
155            fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
156            where
157                E: Error,
158            {
159                let sides = u32::try_from(value)
160                    .map_err(|_| E::custom(format!("PolygonSides value overflow: {value}")))?;
161                RegularPolygonSides::new(sides)
162                    .map(Into::into)
163                    .ok_or_else(|| {
164                        E::custom(format!(
165                            "Regular Polygons cannot have less than 3 sides, got {sides}"
166                        ))
167                    })
168            }
169
170            fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
171            where
172                A: SeqAccess<'de>,
173            {
174                let mut offsets = Vec::new();
175                while let Some(offset) = seq.next_element::<LayerOffset>()? {
176                    offsets.push(offset);
177                }
178                IrregularPolygonSides::new(offsets)
179                    .map(Into::into)
180                    .ok_or_else(|| {
181                        A::Error::custom("Irregular Polygons cannot have less than 3 unique points")
182                    })
183            }
184        }
185
186        deserializer.deserialize_any(PolygonSidesVisitor)
187    }
188
189    /// Serializes [`PolygonSides::Regular`] as a `u32` and [`PolygonSides::Irregular`] as a sequence of offsets.
190    pub fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
191    where
192        S: serde::Serializer,
193    {
194        PolygonSides::serialize_sides(self, serializer)
195    }
196
197    fn serialize_sides<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
198    where
199        S: serde::Serializer,
200    {
201        use serde::ser::SerializeSeq;
202
203        match self {
204            PolygonSides::Regular(r) => serializer.serialize_u32(r.get()),
205            PolygonSides::Irregular(i) => {
206                let offsets = i.as_slice();
207                let mut seq = serializer.serialize_seq(Some(offsets.len()))?;
208                for offset in offsets {
209                    seq.serialize_element(offset)?;
210                }
211                seq.end()
212            }
213        }
214    }
215}
216
217/// An attribute to represent a [`Polygon`] rendered in the layer.
218#[cfg_attr(feature = "pyo3", pyclass(module = "img_gen", from_py_object))]
219#[derive(Debug, Clone, Default, Serialize, Deserialize)]
220pub struct Polygon {
221    /// The border (if specified) ro render around the polygon.
222    #[cfg(feature = "pyo3")]
223    #[pyo3(get, set)]
224    pub border: Option<Border>,
225    /// The [`Border`] (if specified) ro render around the polygon.
226    #[cfg(not(feature = "pyo3"))]
227    pub border: Option<Border>,
228
229    /// The color used to fill the polygon.
230    #[cfg(feature = "pyo3")]
231    #[pyo3(get, set)]
232    #[serde(default = "ColorKind::transparent_default")]
233    pub color: ColorKind,
234
235    /// The color used to fill the polygon.
236    #[cfg(not(feature = "pyo3"))]
237    #[serde(default = "ColorKind::transparent_default")]
238    pub color: ColorKind,
239
240    /// The polygon side definition.
241    ///
242    /// Regular polygons use a side count, while irregular polygons use explicit vertex offsets.
243    #[cfg(feature = "pyo3")]
244    #[pyo3(get, set)]
245    #[serde(
246        default,
247        deserialize_with = "PolygonSides::deserialize",
248        serialize_with = "PolygonSides::serialize"
249    )]
250    pub sides: PolygonSides,
251
252    /// The polygon side definition.
253    ///
254    /// Regular polygons use a side count, while irregular polygons use explicit vertex offsets.
255    #[cfg(not(feature = "pyo3"))]
256    #[serde(
257        default,
258        deserialize_with = "PolygonSides::deserialize",
259        serialize_with = "PolygonSides::serialize"
260    )]
261    pub sides: PolygonSides,
262
263    /// The rotation applied to the rendered polygon.
264    ///
265    /// Does not affect `PolygonSides.Irregular`.
266    #[cfg(feature = "pyo3")]
267    #[pyo3(get, set)]
268    #[serde(default)]
269    pub rotation: f32,
270    /// The rotation applied to the rendered polygon.
271    ///
272    /// Does not affect [`PolygonSides::Irregular`].
273    #[cfg(not(feature = "pyo3"))]
274    #[serde(default)]
275    pub rotation: f32,
276}
277
278#[cfg(test)]
279mod test {
280    #![allow(clippy::unwrap_used, clippy::panic)]
281
282    use super::{IrregularPolygonSides, PolygonSides, RegularPolygonSides};
283    use crate::LayerOffset;
284
285    #[test]
286    fn sides() {
287        assert!(RegularPolygonSides::new(2).is_none());
288        assert!(matches!(PolygonSides::default(), PolygonSides::Regular(v) if v.get() == 3));
289        assert!(IrregularPolygonSides::new(vec![LayerOffset::default(); 2]).is_none());
290
291        assert!(RegularPolygonSides::new(2).is_none());
292        assert!(RegularPolygonSides::new(3).is_some());
293        assert!(IrregularPolygonSides::new(vec![LayerOffset::default(); 2]).is_none());
294        assert!(
295            IrregularPolygonSides::new(vec![
296                LayerOffset { x: 0, y: 0 },
297                LayerOffset { x: 100, y: 0 },
298                LayerOffset { x: 50, y: 100 },
299            ])
300            .is_some()
301        );
302
303        let regular: PolygonSides = RegularPolygonSides::new(3).unwrap().into();
304        assert!(matches!(regular, PolygonSides::Regular(v) if v.get() == 3));
305
306        let irregular: PolygonSides = IrregularPolygonSides::new(vec![
307            LayerOffset { x: 0, y: 0 },
308            LayerOffset { x: 100, y: 0 },
309            LayerOffset { x: 50, y: 100 },
310        ])
311        .unwrap()
312        .into();
313        assert!(matches!(irregular, PolygonSides::Irregular(v) if v.as_slice().len() == 3));
314    }
315
316    #[test]
317    fn deserialize_sides() {
318        use serde::de::{IntoDeserializer, value::U32Deserializer};
319
320        let yaml = 2_u32;
321        let deserializer: U32Deserializer<serde_saphyr::Error> = yaml.into_deserializer();
322        let deserialized = PolygonSides::deserialize(deserializer)
323            .unwrap_err()
324            .to_string();
325        eprintln!("Deserialization error: {deserialized}");
326        assert!(deserialized.contains("Regular Polygons cannot have less than 3 sides"));
327
328        let yaml = 5_u32;
329        let deserializer: U32Deserializer<serde_saphyr::Error> = yaml.into_deserializer();
330        let deserialized = PolygonSides::deserialize(deserializer).unwrap();
331        assert!(matches!(deserialized, PolygonSides::Regular(v) if v.get() == 5));
332    }
333
334    #[test]
335    fn deserialize_irregular_sides() {
336        use serde::Deserialize;
337
338        #[derive(Debug, Deserialize)]
339        struct TestPolygonSides {
340            #[serde(deserialize_with = "PolygonSides::deserialize")]
341            sides: PolygonSides,
342        }
343
344        let expected = [
345            LayerOffset { x: 0, y: 0 },
346            LayerOffset { x: 100, y: 0 },
347            LayerOffset { x: 50, y: 100 },
348        ];
349
350        let yaml = "sides:\n  - { x: 0, y: 0 }\n  - { x: 100, y: 0 }\n  - { x: 50, y: 100 }\n";
351        let deserialized: TestPolygonSides = serde_saphyr::from_str(yaml).unwrap();
352
353        if let PolygonSides::Irregular(points) = deserialized.sides {
354            let points = points.as_slice();
355            assert!(points.len() == expected.len());
356            assert!(points[0].x == expected[0].x && points[0].y == expected[0].y);
357            assert!(points[1].x == expected[1].x && points[1].y == expected[1].y);
358            assert!(points[2].x == expected[2].x && points[2].y == expected[2].y);
359        } else {
360            panic!("Expected irregular polygon sides");
361        }
362    }
363
364    #[test]
365    fn deserialize_irregular_sides_invalid_len() {
366        use serde::de::value::{Error, MapDeserializer, SeqDeserializer};
367
368        let first = MapDeserializer::<_, Error>::new([("x", 0_i32), ("y", 0_i32)].into_iter());
369        let second = MapDeserializer::<_, Error>::new([("x", 100_i32), ("y", 0_i32)].into_iter());
370        let deserializer = SeqDeserializer::<_, Error>::new([first, second].into_iter());
371
372        let error = PolygonSides::deserialize(deserializer)
373            .unwrap_err()
374            .to_string();
375        assert!(error.contains("Irregular Polygons cannot have less than 3 unique points"));
376    }
377
378    #[test]
379    fn deserialize_sides_invalid_str() {
380        use serde::de::value::StrDeserializer;
381
382        let value = StrDeserializer::<serde_saphyr::Error>::new("not-a-valid-polygon-sides");
383        let error = PolygonSides::deserialize(value).unwrap_err().to_string();
384        assert!(error.contains("an integer >= 3 or a sequence of at least 3 unique offsets"));
385    }
386
387    #[test]
388    fn deserialize_regular_polygon_sides_validates_minimum() {
389        use serde::Deserialize;
390        use serde::de::{IntoDeserializer, value::U32Deserializer};
391
392        let deserializer: U32Deserializer<serde_saphyr::Error> = 2_u32.into_deserializer();
393        let error = RegularPolygonSides::deserialize(deserializer)
394            .unwrap_err()
395            .to_string();
396        assert!(error.contains("Regular Polygons cannot have less than 3 sides"));
397    }
398
399    #[test]
400    fn deserialize_irregular_polygon_sides_validates_minimum_unique_points() {
401        let yaml = "- { x: 0, y: 0 }\n- { x: 0, y: 0 }\n- { x: 100, y: 0 }\n";
402        let error = serde_saphyr::from_str::<IrregularPolygonSides>(yaml)
403            .unwrap_err()
404            .to_string();
405        assert!(error.contains("Irregular Polygons cannot have less than 3 unique points"));
406    }
407
408    #[test]
409    fn polygon_sides_deserialize_trait_validates_regular_minimum() {
410        let err = serde_json::from_str::<PolygonSides>("2")
411            .unwrap_err()
412            .to_string();
413        assert!(err.contains("Regular Polygons cannot have less than 3 sides"));
414
415        let parsed = serde_json::from_str::<PolygonSides>("5").unwrap();
416        assert!(matches!(parsed, PolygonSides::Regular(v) if v.get() == 5));
417    }
418
419    #[test]
420    fn polygon_sides_deserialize_trait_validates_irregular_unique_points() {
421        let err = serde_json::from_str::<PolygonSides>(
422            r#"[{"x":0,"y":0},{"x":0,"y":0},{"x":100,"y":0}]"#,
423        )
424        .unwrap_err()
425        .to_string();
426        assert!(err.contains("Irregular Polygons cannot have less than 3 unique points"));
427
428        let parsed = serde_json::from_str::<PolygonSides>(
429            r#"[{"x":0,"y":0},{"x":100,"y":0},{"x":50,"y":100}]"#,
430        )
431        .unwrap();
432        assert!(matches!(parsed, PolygonSides::Irregular(v) if v.as_slice().len() == 3));
433    }
434
435    #[test]
436    fn polygon_sides_serialize_trait_emits_expected_shape() {
437        let regular: PolygonSides = RegularPolygonSides::new(6).unwrap().into();
438        assert_eq!(
439            serde_json::to_value(&regular).unwrap(),
440            serde_json::json!(6)
441        );
442
443        let irregular: PolygonSides = IrregularPolygonSides::new(vec![
444            LayerOffset { x: 0, y: 0 },
445            LayerOffset { x: 100, y: 0 },
446            LayerOffset { x: 50, y: 100 },
447        ])
448        .unwrap()
449        .into();
450        assert_eq!(
451            serde_json::to_value(&irregular).unwrap(),
452            serde_json::json!([
453                { "x": 0, "y": 0 },
454                { "x": 100, "y": 0 },
455                { "x": 50, "y": 100 }
456            ])
457        );
458    }
459}