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