microcad_lang/eval/
attribute.rs

1// Copyright © 2025 The µcad authors <info@ucad.xyz>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4use std::str::FromStr;
5
6use crate::{
7    builtin::ExporterAccess,
8    eval::{self, *},
9    model::{Attributes, CustomCommand, ExportCommand, MeasureCommand, ResolutionAttribute},
10    parameter,
11    syntax::{self, *},
12    Id,
13};
14
15use microcad_core::{theme::Theme, Color, RenderResolution, Size2};
16use thiserror::Error;
17
18/// Error type for attributes.
19#[derive(Debug, Error)]
20pub enum AttributeError {
21    /// Unknown attribute.
22    #[error("Attribute not supported: {0}")]
23    NotSupported(Identifier),
24
25    /// Attribute cannot be assigned to an expression.
26    #[error("Cannot assign attribute to expression `{0}`")]
27    CannotAssignAttribute(Box<Expression>),
28
29    /// Warning when an attribute has already been set.
30    #[error("The attribute is already set: {0} = {1}")]
31    AttributeAlreadySet(Identifier, Value),
32
33    /// The attribute was not found.
34    #[error("Not found: {0}")]
35    NotFound(Identifier),
36
37    /// Invalid command.
38    #[error("Invalid command list for attribute `{0}`")]
39    InvalidCommand(Identifier),
40}
41
42impl Eval<Option<ExportCommand>> for syntax::AttributeCommand {
43    fn eval(&self, context: &mut Context) -> EvalResult<Option<ExportCommand>> {
44        match self {
45            AttributeCommand::Call(_, Some(argument_list)) => {
46                match ArgumentMatch::find_match(
47                    &argument_list.eval(context)?,
48                    &[
49                        parameter!(filename: String),
50                        parameter!(resolution: Length = 0.1 /*mm*/),
51                        (
52                            Identifier::no_ref("size"),
53                            eval::ParameterValue {
54                                specified_type: Some(Type::Tuple(Box::new(TupleType::new_size2()))),
55                                default_value: Some(Value::Tuple(Box::new(Size2::A4.into()))),
56                                src_ref: SrcRef(None),
57                            },
58                        ),
59                    ]
60                    .into_iter()
61                    .collect(),
62                ) {
63                    Ok(arguments) => {
64                        let filename: std::path::PathBuf =
65                            arguments.get::<String>("filename").into();
66                        let id: Option<Id> = if let Ok(id) = arguments.by_str::<String>("id") {
67                            Some(id.into())
68                        } else {
69                            None
70                        };
71                        let resolution = RenderResolution::new(
72                            arguments.get::<&Value>("resolution").try_scalar()?,
73                        );
74
75                        match context.find_exporter(&filename, &id) {
76                            Ok(exporter) => Ok(Some(ExportCommand {
77                                filename,
78                                exporter,
79                                resolution,
80                            })),
81                            Err(err) => {
82                                context.warning(self, err)?;
83                                Ok(None)
84                            }
85                        }
86                    }
87                    Err(err) => {
88                        context.warning(self, err)?;
89                        Ok(None)
90                    }
91                }
92            }
93            AttributeCommand::Expression(expression) => {
94                let value: Value = expression.eval(context)?;
95                match value {
96                    Value::String(filename) => {
97                        let filename = std::path::PathBuf::from(filename);
98                        match context.find_exporter(&filename, &None) {
99                            Ok(exporter) => Ok(Some(ExportCommand {
100                                filename,
101                                resolution: RenderResolution::default(),
102                                exporter,
103                            })),
104                            Err(err) => {
105                                context.warning(self, err)?;
106                                Ok(None)
107                            }
108                        }
109                    }
110                    _ => unimplemented!(),
111                }
112            }
113            _ => Ok(None),
114        }
115    }
116}
117
118impl Eval<Vec<ExportCommand>> for syntax::Attribute {
119    fn eval(&self, context: &mut Context) -> EvalResult<Vec<ExportCommand>> {
120        assert_eq!(self.id.id().as_str(), "export");
121
122        self.commands
123            .iter()
124            .try_fold(Vec::new(), |mut commands, attribute| {
125                if let Some(export_command) = attribute.eval(context)? {
126                    commands.push(export_command)
127                }
128                Ok(commands)
129            })
130    }
131}
132
133impl Eval<Vec<MeasureCommand>> for syntax::Attribute {
134    fn eval(&self, context: &mut Context) -> EvalResult<Vec<MeasureCommand>> {
135        let mut commands = Vec::new();
136
137        for command in &self.commands {
138            match command {
139                AttributeCommand::Call(Some(id), _) => match id.id().as_str() {
140                    "width" => commands.push(MeasureCommand::Width),
141                    "height" => commands.push(MeasureCommand::Height),
142                    "size" => commands.push(MeasureCommand::Size),
143                    _ => context.warning(self, AttributeError::InvalidCommand(id.clone()))?,
144                },
145                _ => unimplemented!(),
146            }
147        }
148
149        Ok(commands)
150    }
151}
152
153impl Eval<Vec<CustomCommand>> for syntax::Attribute {
154    fn eval(&self, context: &mut Context) -> EvalResult<Vec<CustomCommand>> {
155        match context.exporters().exporter_by_id(self.id.id()) {
156            Ok(exporter) => {
157                let mut commands = Vec::new();
158                for command in &self.commands {
159                    match command {
160                        AttributeCommand::Call(None, Some(argument_list)) => {
161                            match ArgumentMatch::find_match(
162                                &argument_list.eval(context)?,
163                                &exporter.model_parameters(),
164                            ) {
165                                Ok(tuple) => commands.push(CustomCommand {
166                                    id: self.id.clone(),
167                                    arguments: Box::new(tuple),
168                                }),
169                                Err(err) => {
170                                    context.warning(self, err)?;
171                                }
172                            }
173                        }
174                        _ => unimplemented!(),
175                    }
176                }
177
178                Ok(commands)
179            }
180            Err(err) => {
181                context.warning(self, err)?;
182                Ok(Vec::default())
183            }
184        }
185    }
186}
187
188impl Eval<Option<Color>> for syntax::AttributeCommand {
189    fn eval(&self, context: &mut Context) -> EvalResult<Option<Color>> {
190        match self {
191            AttributeCommand::Call(_, _) => todo!(),
192            // Get color from a tuple or string.
193            AttributeCommand::Expression(expression) => {
194                let value: Value = expression.eval(context)?;
195                match value {
196                    // Color from string: color = "red"
197                    Value::String(s) => match Color::from_str(&s) {
198                        Ok(color) => Ok(Some(color)),
199                        Err(err) => {
200                            context.warning(self, err)?;
201                            Ok(None)
202                        }
203                    },
204                    // Color from tuple: color = (r = 1.0, g = 1.0, b = 1.0, a = 1.0)
205                    Value::Tuple(tuple) => match Color::try_from(tuple.as_ref()) {
206                        Ok(color) => Ok(Some(color)),
207                        Err(err) => {
208                            context.warning(self, err)?;
209                            Ok(None)
210                        }
211                    },
212                    _ => {
213                        context.warning(
214                            self,
215                            AttributeError::InvalidCommand(Identifier::no_ref("color")),
216                        )?;
217                        Ok(None)
218                    }
219                }
220            }
221        }
222    }
223}
224
225impl Eval<Option<ResolutionAttribute>> for syntax::AttributeCommand {
226    fn eval(&self, context: &mut Context) -> EvalResult<Option<ResolutionAttribute>> {
227        match self {
228            AttributeCommand::Expression(expression) => {
229                let value: Value = expression.eval(context)?;
230                match value {
231                    Value::Quantity(qty) => match qty.quantity_type {
232                        QuantityType::Scalar => Ok(Some(ResolutionAttribute::Relative(qty.value))),
233                        QuantityType::Length => Ok(Some(ResolutionAttribute::Linear(qty.value))),
234                        _ => unimplemented!(),
235                    },
236                    _ => todo!("Error handling"),
237                }
238            }
239            AttributeCommand::Call(_, _) => {
240                context.warning(
241                    self,
242                    AttributeError::InvalidCommand(Identifier::no_ref("resolution")),
243                )?;
244                Ok(None)
245            }
246        }
247    }
248}
249
250impl Eval<Option<std::rc::Rc<Theme>>> for syntax::AttributeCommand {
251    fn eval(&self, context: &mut Context) -> EvalResult<Option<std::rc::Rc<Theme>>> {
252        match self {
253            AttributeCommand::Expression(_) => todo!(),
254            AttributeCommand::Call(_, _) => {
255                context.warning(
256                    self,
257                    AttributeError::InvalidCommand(Identifier::no_ref("resolution")),
258                )?;
259                Ok(None)
260            }
261        }
262    }
263}
264
265impl Eval<Option<Size2>> for syntax::AttributeCommand {
266    fn eval(&self, _: &mut Context) -> EvalResult<Option<Size2>> {
267        todo!("Get Size2, e.g. `size = (width = 10mm, height = 10mm) from AttributeCommand")
268    }
269}
270
271macro_rules! eval_to_attribute {
272    ($id:ident: $ty:ty) => {
273        impl Eval<Option<$ty>> for syntax::Attribute {
274            fn eval(&self, context: &mut Context) -> EvalResult<Option<$ty>> {
275                assert_eq!(self.id.id().as_str(), stringify!($id));
276                match self.single_command() {
277                    Some(command) => Ok(command.eval(context)?),
278                    None => {
279                        context.warning(self, AttributeError::InvalidCommand(self.id.clone()))?;
280                        Ok(None)
281                    }
282                }
283            }
284        }
285    };
286}
287
288eval_to_attribute!(color: Color);
289eval_to_attribute!(resolution: ResolutionAttribute);
290eval_to_attribute!(theme: std::rc::Rc<Theme>);
291eval_to_attribute!(size: Size2);
292
293impl Eval<Vec<crate::model::Attribute>> for syntax::Attribute {
294    fn eval(&self, context: &mut Context) -> EvalResult<Vec<crate::model::Attribute>> {
295        let id = self.id.id().as_str();
296        use crate::model::Attribute as Attr;
297        Ok(match id {
298            "color" => match self.eval(context)? {
299                Some(color) => vec![Attr::Color(color)],
300                None => Default::default(),
301            },
302            "resolution" => match self.eval(context)? {
303                Some(resolution) => vec![Attr::Resolution(resolution)],
304                None => Default::default(),
305            },
306            "theme" => match self.eval(context)? {
307                Some(theme) => vec![Attr::Theme(theme)],
308                None => Default::default(),
309            },
310            "size" => match self.eval(context)? {
311                Some(size) => vec![Attr::Size(size)],
312                None => Default::default(),
313            },
314            "export" => {
315                let exports: Vec<ExportCommand> = self.eval(context)?;
316                exports.iter().cloned().map(Attr::Export).collect()
317            }
318            "measure" => {
319                let measures: Vec<MeasureCommand> = self.eval(context)?;
320                measures.iter().cloned().map(Attr::Measure).collect()
321            }
322            _ => {
323                let commands: Vec<CustomCommand> = self.eval(context)?;
324                commands.iter().cloned().map(Attr::Custom).collect()
325            }
326        })
327    }
328}
329
330impl Eval<crate::model::Attributes> for AttributeList {
331    fn eval(&self, context: &mut Context) -> EvalResult<crate::model::Attributes> {
332        Ok(Attributes(self.iter().try_fold(
333            Vec::new(),
334            |mut attributes, attribute| -> EvalResult<_> {
335                attributes.append(&mut attribute.eval(context)?);
336                Ok(attributes)
337            },
338        )?))
339    }
340}