Skip to main content

microcad_lang/model/attribute/
mod.rs

1// Copyright © 2025-2026 The µcad authors <info@microcad.xyz>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4//! Model attributes.
5
6mod attributes;
7mod export_command;
8mod layer;
9mod measure_command;
10mod resolution_attribute;
11
12pub use attributes::Attributes;
13pub use export_command::ExportCommand;
14pub use layer::Layer;
15pub use measure_command::MeasureCommand;
16pub use resolution_attribute::ResolutionAttribute;
17
18use crate::value::*;
19
20use microcad_core::{Color, Size2};
21use microcad_lang_base::Identifier;
22use microcad_lang_proc_macros::Identifiable;
23
24/// A custom command attribute from an exporter, e.g.: `svg = (style = "fill:none")`
25#[derive(Clone, Debug, Identifiable)]
26pub struct CustomCommand {
27    /// Attribute id.
28    id: Identifier,
29    /// Argument tuple.
30    pub arguments: Box<Tuple>,
31}
32
33impl CustomCommand {
34    pub(crate) fn new(id: Identifier, arguments: Tuple) -> CustomCommand {
35        CustomCommand {
36            id,
37            arguments: arguments.into(),
38        }
39    }
40}
41
42impl std::fmt::Display for CustomCommand {
43    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44        write!(f, "{} = {}", self.id, self.arguments)
45    }
46}
47
48/// An attribute for a model.
49#[derive(Clone, Debug)]
50pub enum Attribute {
51    /// Color attribute: `color = "red"`
52    Color(Color),
53    /// Render resolution attribute: `resolution = 200%`.
54    Resolution(ResolutionAttribute),
55    /// Size attribute: `size = std::A4`.
56    Size(Size2),
57    /// Export command: `export = "test.svg"`.
58    Export(ExportCommand),
59    /// Measure command: `measure = width`
60    Measure(MeasureCommand),
61    /// Custom non-builtin attribute with tuples: svg = (fill = "color"))
62    Custom(CustomCommand),
63}
64
65impl Attribute {
66    /// Return an id for the attribute.
67    fn id(&self) -> Identifier {
68        match &self {
69            Attribute::Color(_) => Identifier::no_ref("color"),
70            Attribute::Resolution(_) => Identifier::no_ref("resolution"),
71            Attribute::Size(_) => Identifier::no_ref("size"),
72            Attribute::Export(_) => Identifier::no_ref("export"),
73            Attribute::Measure(_) => Identifier::no_ref("measure"),
74            Attribute::Custom(attr) => attr.id.clone(),
75        }
76    }
77
78    /// If this method returns true, the attribute can only be set once.
79    pub fn is_unique(&self) -> bool {
80        matches!(
81            self,
82            Attribute::Color(_) | Attribute::Resolution(_) | Attribute::Size(_)
83        )
84    }
85}
86
87impl std::fmt::Display for Attribute {
88    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
89        write!(
90            f,
91            "#[{id} = {value}]",
92            id = self.id(),
93            value = match &self {
94                Attribute::Color(color) => format!("{color}"),
95                Attribute::Resolution(resolution) => format!("{resolution}"),
96                Attribute::Size(size) => format!("{size}"),
97                Attribute::Export(export) => format!("{export}"),
98                Attribute::Measure(measure) => format!("{measure}"),
99                Attribute::Custom(command) => format!("{command}"),
100            }
101        )
102    }
103}
104
105/// This trait implementation is used to access values from an attribute.
106impl From<Attribute> for Value {
107    fn from(value: Attribute) -> Self {
108        match value {
109            Attribute::Color(color) => Value::Tuple(Box::new(color.into())),
110            Attribute::Resolution(resolution_attribute) => resolution_attribute.into(),
111            Attribute::Size(size) => size.into(),
112            Attribute::Export(e) => e.into(),
113            Attribute::Measure(m) => m.into(),
114            Attribute::Custom(attr) => Value::Tuple(attr.arguments.clone()),
115        }
116    }
117}
118
119impl PartialEq for Attribute {
120    fn eq(&self, other: &Self) -> bool {
121        self.id() == other.id()
122    }
123}
124
125/// Access an attributes value by id.
126pub trait AttributesAccess {
127    /// Get a value attribute by id.
128    fn get_attributes_by_id(&self, id: &Identifier) -> Vec<Attribute>;
129
130    /// Get a single attributes.
131    fn get_single_attribute(&self, id: &Identifier) -> Option<Attribute> {
132        let attributes = self.get_attributes_by_id(id);
133        match attributes.len() {
134            1 => attributes.first().cloned(),
135            _ => None,
136        }
137    }
138
139    /// Get single attribute as value.
140    fn get_attribute_value(&self, id: &Identifier) -> Value {
141        match self.get_single_attribute(id) {
142            Some(attribute) => attribute.into(),
143            None => Value::None,
144        }
145    }
146
147    /// Get resolution attribute.
148    fn get_resolution(&self) -> Option<ResolutionAttribute> {
149        match self.get_single_attribute(&Identifier::no_ref("resolution")) {
150            Some(value) => match value {
151                Attribute::Resolution(resolution) => Some(resolution),
152                _ => unreachable!(),
153            },
154            None => None,
155        }
156    }
157
158    /// Color (builtin attribute).
159    fn get_color(&self) -> Option<Color> {
160        match self.get_single_attribute(&Identifier::no_ref("color")) {
161            Some(value) => match value {
162                Attribute::Color(color) => Some(color),
163                _ => unreachable!(),
164            },
165            None => None,
166        }
167    }
168
169    /// Get size.
170    fn get_size(&self) -> Option<Size2> {
171        self.get_single_attribute(&Identifier::no_ref("size"))
172            .map(|attr| match attr {
173                Attribute::Size(size) => size,
174                _ => unreachable!(),
175            })
176    }
177
178    /// Get all export commands.
179    fn get_exports(&self) -> Vec<ExportCommand> {
180        self.get_attributes_by_id(&Identifier::no_ref("export"))
181            .into_iter()
182            .fold(Vec::new(), |mut exports, command| {
183                match command {
184                    Attribute::Export(export_command) => exports.push(export_command.clone()),
185                    _ => unreachable!(),
186                }
187                exports
188            })
189    }
190
191    /// Get all measure commands.
192    fn get_measures(&self) -> Vec<MeasureCommand> {
193        self.get_attributes_by_id(&Identifier::no_ref("measure"))
194            .iter()
195            .fold(Vec::new(), |mut measures, attribute| {
196                match attribute {
197                    Attribute::Measure(measure_command) => measures.push(measure_command.clone()),
198                    _ => unreachable!(),
199                }
200                measures
201            })
202    }
203
204    /// Get custom attributes.
205    fn get_custom_attributes(&self, id: &Identifier) -> Vec<Tuple> {
206        self.get_attributes_by_id(id)
207            .iter()
208            .fold(Vec::new(), |mut attributes, attribute| {
209                match attribute {
210                    Attribute::Custom(attr) => attributes.push(attr.arguments.as_ref().clone()),
211                    _ => unreachable!(),
212                }
213                attributes
214            })
215    }
216}