Skip to main content

dotloom_document/
value.rs

1//! Typed property values, colors and styles.
2
3use core::fmt;
4
5use dotloom_geometry::Point;
6use serde::{Deserialize, Serialize};
7
8use crate::{DocError, EntityId};
9
10/// Reference to another entity: `{"ref": 12}`.
11#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
12pub struct RefValue {
13    /// Target entity.
14    #[serde(rename = "ref")]
15    pub entity: EntityId,
16}
17
18/// A named anchor of an entity: `{"entity": 12, "anchor": "start"}`.
19#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
20pub struct AnchorRef {
21    /// Entity.
22    pub entity: EntityId,
23    /// Anchor name (`start`, `end`, `mid`, `center`, `v3`, plugin-defined names).
24    pub anchor: String,
25}
26
27impl AnchorRef {
28    /// Create an anchor reference.
29    #[must_use]
30    pub fn new(entity: EntityId, anchor: impl Into<String>) -> Self {
31        Self { entity, anchor: anchor.into() }
32    }
33}
34
35/// A typed property value. Numbers are stored in canonical units (mm, rad, s); the
36/// dimension of each property comes from its type definition.
37#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
38#[serde(untagged)]
39pub enum PropValue {
40    /// Boolean.
41    Bool(bool),
42    /// Number in canonical units.
43    Number(f64),
44    /// String or enum value.
45    Text(String),
46    /// Point `[x, y]`.
47    Point(Point),
48    /// Entity reference.
49    Ref(RefValue),
50    /// Anchor reference.
51    Anchor(AnchorRef),
52}
53
54impl PropValue {
55    /// Number value.
56    #[must_use]
57    pub fn as_number(&self) -> Option<f64> {
58        if let Self::Number(v) = self { Some(*v) } else { None }
59    }
60
61    /// Referenced entity, if this is a reference.
62    #[must_use]
63    pub fn referenced_entity(&self) -> Option<EntityId> {
64        match self {
65            Self::Ref(r) => Some(r.entity),
66            Self::Anchor(a) => Some(a.entity),
67            _ => None,
68        }
69    }
70
71    /// Whether all numbers are finite.
72    #[must_use]
73    pub fn is_finite(&self) -> bool {
74        match self {
75            Self::Number(v) => v.is_finite(),
76            Self::Point(p) => p.is_finite(),
77            _ => true,
78        }
79    }
80
81    /// Kind name for diagnostics.
82    #[must_use]
83    pub const fn kind_name(&self) -> &'static str {
84        match self {
85            Self::Bool(_) => "bool",
86            Self::Number(_) => "number",
87            Self::Text(_) => "text",
88            Self::Point(_) => "point",
89            Self::Ref(_) => "ref",
90            Self::Anchor(_) => "anchor",
91        }
92    }
93}
94
95/// RGBA color, serialized as `#rrggbb` or `#rrggbbaa`.
96#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
97#[serde(try_from = "String", into = "String")]
98pub struct Color(pub u32);
99
100impl Color {
101    /// Opaque black.
102    pub const BLACK: Self = Self(0x0000_00ff);
103
104    /// From components.
105    #[must_use]
106    pub const fn rgba(r: u8, g: u8, b: u8, a: u8) -> Self {
107        Self(((r as u32) << 24) | ((g as u32) << 16) | ((b as u32) << 8) | a as u32)
108    }
109
110    /// Components `[r, g, b, a]`.
111    #[must_use]
112    pub const fn components(self) -> [u8; 4] {
113        self.0.to_be_bytes()
114    }
115
116    /// Parse `#rgb`, `#rrggbb` or `#rrggbbaa`.
117    pub fn parse(s: &str) -> Result<Self, DocError> {
118        let hex = s.strip_prefix('#').ok_or_else(|| DocError::InvalidValue(format!("color `{s}`")))?;
119        let bad = || DocError::InvalidValue(format!("color `{s}`"));
120        if !hex.chars().all(|c| c.is_ascii_hexdigit()) {
121            return Err(bad());
122        }
123        let v = u32::from_str_radix(hex, 16).map_err(|_| bad())?;
124        match hex.len() {
125            3 => {
126                let r = (v >> 8) & 0xf;
127                let g = (v >> 4) & 0xf;
128                let b = v & 0xf;
129                Ok(Self((r * 17) << 24 | (g * 17) << 16 | (b * 17) << 8 | 0xff))
130            }
131            6 => Ok(Self(v << 8 | 0xff)),
132            8 => Ok(Self(v)),
133            _ => Err(bad()),
134        }
135    }
136}
137
138impl fmt::Display for Color {
139    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
140        let [r, g, b, a] = self.components();
141        if a == 0xff { write!(f, "#{r:02x}{g:02x}{b:02x}") } else { write!(f, "#{r:02x}{g:02x}{b:02x}{a:02x}") }
142    }
143}
144
145impl TryFrom<String> for Color {
146    type Error = DocError;
147    fn try_from(s: String) -> Result<Self, DocError> {
148        Self::parse(&s)
149    }
150}
151
152impl From<Color> for String {
153    fn from(c: Color) -> Self {
154        c.to_string()
155    }
156}
157
158/// Visual style of an entity. Unset fields inherit from the layer, then the theme.
159#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
160#[serde(rename_all = "camelCase")]
161pub struct Style {
162    /// Stroke color.
163    #[serde(default, skip_serializing_if = "Option::is_none")]
164    pub stroke: Option<Color>,
165    /// Fill color (regions only).
166    #[serde(default, skip_serializing_if = "Option::is_none")]
167    pub fill: Option<Color>,
168    /// Stroke width in CSS pixels (screen-constant).
169    #[serde(default, skip_serializing_if = "Option::is_none")]
170    pub stroke_width: Option<f64>,
171    /// Dash pattern in CSS pixels.
172    #[serde(default, skip_serializing_if = "Option::is_none")]
173    pub dash: Option<Vec<f64>>,
174}
175
176impl Style {
177    /// Whether every field is unset.
178    #[must_use]
179    pub fn is_default(&self) -> bool {
180        *self == Self::default()
181    }
182
183    /// Validate numeric fields.
184    pub fn validate(&self) -> Result<(), DocError> {
185        if let Some(w) = self.stroke_width
186            && !(w.is_finite() && (0.0..=1000.0).contains(&w))
187        {
188            return Err(DocError::InvalidValue("stroke width must be within 0..=1000 px".into()));
189        }
190        if let Some(d) = &self.dash
191            && (d.len() > 16 || d.iter().any(|v| !(v.is_finite() && *v >= 0.0)))
192        {
193            return Err(DocError::InvalidValue("dash pattern".into()));
194        }
195        Ok(())
196    }
197}
198
199#[cfg(test)]
200mod tests {
201    use super::*;
202
203    #[test]
204    fn prop_values_roundtrip_untagged() {
205        let vals = vec![
206            PropValue::Bool(true),
207            PropValue::Number(600.0),
208            PropValue::Text("oak".into()),
209            PropValue::Point(Point::new(1.0, 2.0)),
210            PropValue::Ref(RefValue { entity: EntityId(4) }),
211            PropValue::Anchor(AnchorRef::new(EntityId(4), "start")),
212        ];
213        let j = serde_json::to_string(&vals).unwrap();
214        assert_eq!(j, r#"[true,600.0,"oak",[1.0,2.0],{"ref":4},{"entity":4,"anchor":"start"}]"#);
215        let back: Vec<PropValue> = serde_json::from_str(&j).unwrap();
216        assert_eq!(back, vals);
217    }
218
219    #[test]
220    fn colors() {
221        assert_eq!(Color::parse("#fff").unwrap(), Color(0xffff_ffff));
222        assert_eq!(Color::parse("#1a2b3c").unwrap().to_string(), "#1a2b3c");
223        assert_eq!(Color::parse("#1a2b3c80").unwrap().to_string(), "#1a2b3c80");
224        assert!(Color::parse("1a2b3c").is_err());
225        assert!(Color::parse("#12345").is_err());
226        assert!(Color::parse("#+12345").is_err());
227    }
228}