microcad_lang/model/attribute/
mod.rs

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