microcad_export/svg/
attributes.rs

1// Copyright © 2025 The µcad authors <info@ucad.xyz>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4//! Scalable Vector Graphics (SVG) tag attributes
5
6use derive_more::Deref;
7use microcad_core::{Color, Mat3, Scalar};
8use microcad_lang::{
9    model::{AttributesAccess, Model},
10    syntax::Identifier,
11    value::{Value, ValueAccess},
12};
13
14#[derive(Debug, Clone)]
15pub enum SvgTagAttribute {
16    /// `marker-start` attribute, e.g. for arrow heads.
17    MarkerStart(String),
18
19    /// `marker-end` attribute, e.g. for arrow heads.
20    MarkerEnd(String),
21
22    /// Style attribute: `style = "fill: skyblue; stroke: cadetblue; stroke-width: 2;"`.
23    Style {
24        fill: Option<Color>,
25        stroke: Option<Color>,
26        stroke_width: Option<Scalar>,
27    },
28
29    /// Transform by mat3 matrix attribute.
30    Transform(Mat3),
31
32    /// Class attribute.
33    Class(String),
34
35    /// Custom attribute
36    Custom(String, String),
37}
38
39impl SvgTagAttribute {
40    /// Class constructor.
41    pub fn class(s: &str) -> Self {
42        Self::Class(s.to_string())
43    }
44
45    /// Style constructor.
46    pub fn style(fill: Option<Color>, stroke: Option<Color>, stroke_width: Option<Scalar>) -> Self {
47        Self::Style {
48            fill,
49            stroke,
50            stroke_width,
51        }
52    }
53
54    fn id(&self) -> &str {
55        match &self {
56            SvgTagAttribute::MarkerStart(_) => "marker-start",
57            SvgTagAttribute::MarkerEnd(_) => "marker-end",
58            SvgTagAttribute::Style {
59                fill: _,
60                stroke: _,
61                stroke_width: _,
62            } => "style",
63            SvgTagAttribute::Transform(_) => "transform",
64            SvgTagAttribute::Class(_) => "class",
65            SvgTagAttribute::Custom(id, _) => id,
66        }
67    }
68}
69
70impl std::fmt::Display for SvgTagAttribute {
71    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
72        let value = match &self {
73            SvgTagAttribute::MarkerStart(marker_name) | SvgTagAttribute::MarkerEnd(marker_name) => {
74                format!("url(#{marker_name})")
75            }
76            SvgTagAttribute::Style {
77                fill,
78                stroke,
79                stroke_width,
80            } => format!(
81                "{fill}{stroke}{stroke_width}",
82                fill = match fill {
83                    Some(fill) => format!("fill: {}; ", fill.to_svg_color()),
84                    None => "fill: none; ".into(),
85                },
86                stroke = match stroke {
87                    Some(stroke) => format!("stroke: {}; ", stroke.to_svg_color()),
88                    None => "stroke: none; ".into(),
89                },
90                stroke_width = match stroke_width {
91                    Some(stroke_width) => format!("stroke-width: {stroke_width}"),
92                    None => String::new(),
93                }
94            ),
95            SvgTagAttribute::Transform(m) => {
96                let (a, b, c, d, e, f) = (m.x.x, m.x.y, m.y.x, m.y.y, m.z.x, m.z.y);
97                format!("matrix({a} {b} {c} {d} {e} {f})")
98            }
99            SvgTagAttribute::Class(class) => class.clone(),
100            SvgTagAttribute::Custom(_, value) => value.clone(),
101        };
102
103        write!(f, "{}=\"{value}\"", self.id(),)
104    }
105}
106
107/// Tag attributes for an SVG tag.
108#[derive(Debug, Clone, Default, Deref)]
109pub struct SvgTagAttributes(std::collections::BTreeMap<String, SvgTagAttribute>);
110
111/// Generic methods.
112impl SvgTagAttributes {
113    /// Merge tags with others.
114    pub fn merge(mut self, mut other: Self) -> Self {
115        self.0.append(&mut other.0);
116        self
117    }
118}
119
120/// Methods for inserting specific tag attributes.
121impl SvgTagAttributes {
122    /// Insert new tag attribute.
123    pub fn insert(mut self, attr: SvgTagAttribute) -> Self {
124        match self.0.get_mut(attr.id()) {
125            Some(SvgTagAttribute::Class(class)) => match attr {
126                SvgTagAttribute::Class(new_class) => {
127                    *class += &format!(" {new_class}");
128                }
129                _ => unreachable!(),
130            },
131            _ => {
132                self.0.insert(attr.id().to_string(), attr);
133            }
134        }
135
136        self
137    }
138
139    /// Apply SVG attributes from model attributes
140    pub fn apply_from_model(mut self, model: &Model) -> Self {
141        if let Some(color) = model.get_color() {
142            self = self.insert(SvgTagAttribute::Style {
143                fill: Some(color),
144                stroke: None,
145                stroke_width: None,
146            });
147        }
148
149        model
150            .get_custom_attributes(&Identifier::no_ref("svg"))
151            .iter()
152            .for_each(|tuple| {
153                if let Some(Value::String(style)) = tuple.by_id(&Identifier::no_ref("style")) {
154                    self = self
155                        .clone()
156                        .insert(SvgTagAttribute::Custom("style".into(), style.clone()));
157                }
158                if let Some(Value::String(fill)) = tuple.by_id(&Identifier::no_ref("fill")) {
159                    self = self
160                        .clone()
161                        .insert(SvgTagAttribute::Custom("fill".into(), fill.clone()));
162                }
163            });
164        self
165    }
166}
167
168impl std::fmt::Display for SvgTagAttributes {
169    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
170        write!(
171            f,
172            "{}",
173            self.0
174                .values()
175                .map(|attr| attr.to_string())
176                .collect::<Vec<_>>()
177                .join(" ")
178        )
179    }
180}
181
182impl From<SvgTagAttribute> for SvgTagAttributes {
183    fn from(value: SvgTagAttribute) -> Self {
184        [value].into_iter().collect()
185    }
186}
187
188impl FromIterator<SvgTagAttribute> for SvgTagAttributes {
189    fn from_iter<T: IntoIterator<Item = SvgTagAttribute>>(iter: T) -> Self {
190        let mut s = Self::default();
191        iter.into_iter().for_each(|attr| {
192            s.0.insert(attr.id().to_string(), attr);
193        });
194        s
195    }
196}
197
198impl<'a> FromIterator<(&'a str, &'a str)> for SvgTagAttributes {
199    fn from_iter<T: IntoIterator<Item = (&'a str, &'a str)>>(iter: T) -> Self {
200        let mut s = Self::default();
201        iter.into_iter().for_each(|(key, value)| {
202            let key = key.to_string();
203            s.0.insert(key.clone(), SvgTagAttribute::Custom(key, value.to_string()));
204        });
205        s
206    }
207}