microcad_lang/builtin/
export.rs

1// Copyright © 2025 The µcad authors <info@ucad.xyz>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4//! Model export
5
6use std::rc::Rc;
7
8use crate::{Id, builtin::file_io::*, model::*, parameter, render::RenderError, value::*};
9
10use thiserror::Error;
11
12/// Export error stub.
13#[derive(Error, Debug)]
14pub enum ExportError {
15    /// IO Error.
16    #[error("IO Error")]
17    IoError(#[from] std::io::Error),
18
19    /// Format Error.
20    #[error("Format Error")]
21    FormatError(#[from] std::fmt::Error),
22
23    /// The model does not contain any export attribute.
24    #[error("No export attribute found in workbench (mark it with `#[export(\"filename\")`")]
25    NoExportAttribute,
26
27    /// No exporter found for file.
28    #[error("No exporter found for file `{0}`")]
29    NoExporterForFile(std::path::PathBuf),
30
31    /// No exporter for id.
32    #[error("No exporter found with id `{0}`")]
33    NoExporterWithId(Id),
34
35    /// No exporter id.
36    #[error("Multiple exporters for file extension: {0:?}")]
37    MultipleExportersForFileExtension(Vec<Id>),
38
39    /// Render error during export.
40    #[error("Render error: {0}")]
41    RenderError(#[from] RenderError),
42}
43
44/// Exporter trait.
45///
46/// Implement this trait for your custom file exporter.
47pub trait Exporter: FileIoInterface {
48    /// Parameters that add exporter specific attributes to a model.
49    ///
50    /// Let's assume an exporter `foo` has a model parameter `bar = 23` as parameter value list.
51    /// The parameter `bar` can be set to `42` with:
52    ///
53    /// ```ucad
54    /// #[export = "myfile.foo"]
55    /// #[foo = (bar = 42)]
56    /// Circle(42mm);
57    /// ```
58    fn model_parameters(&self) -> ParameterValueList {
59        ParameterValueList::default()
60    }
61
62    /// Parameters for the export attribute: `export = svg("filename.svg")`
63    fn export_parameters(&self) -> ParameterValueList {
64        [parameter!(filename: String), parameter!(resolution: Length)]
65            .into_iter()
66            .collect()
67    }
68
69    /// Export the model if the model is marked for export.
70    fn export(&self, model: &Model, filename: &std::path::Path) -> Result<Value, ExportError>;
71
72    /// The expected model output type of this exporter.
73    ///
74    /// Reimplement this function when your export output format only accepts specific model output types.
75    fn output_type(&self) -> OutputType {
76        OutputType::NotDetermined
77    }
78}
79
80/// Exporter registry.
81///
82/// A database in which all exporters are stored.
83///
84/// The registry is used to find exporters by their id and their file extension.
85#[derive(Default)]
86pub struct ExporterRegistry {
87    io: FileIoRegistry<Rc<dyn Exporter>>,
88}
89
90impl ExporterRegistry {
91    /// Create new registry.
92    pub fn new() -> Self {
93        Self {
94            io: FileIoRegistry::default(),
95        }
96    }
97
98    /// Add new exporter to the registry.
99    ///
100    /// TODO Error handling.
101    pub fn insert(mut self, exporter: impl Exporter + 'static) -> Self {
102        let rc = Rc::new(exporter);
103        self.io.insert(rc);
104        self
105    }
106
107    /// Get exporter by filename.
108    pub fn by_filename(
109        &self,
110        filename: impl AsRef<std::path::Path>,
111    ) -> Result<Rc<dyn Exporter>, ExportError> {
112        let importers = self.io.by_filename(filename.as_ref());
113        match importers.len() {
114            0 => Err(ExportError::NoExporterForFile(std::path::PathBuf::from(
115                filename.as_ref(),
116            ))),
117            1 => Ok(importers.first().expect("One importer").clone()),
118            _ => Err(ExportError::MultipleExportersForFileExtension(
119                importers.iter().map(|importer| importer.id()).collect(),
120            )),
121        }
122    }
123}
124
125/// Exporter access.
126pub trait ExporterAccess {
127    /// Get exporter by id.
128    fn exporter_by_id(&self, id: &Id) -> Result<Rc<dyn Exporter>, ExportError>;
129
130    /// Get exporter by filename.
131    fn exporter_by_filename(
132        &self,
133        filename: &std::path::Path,
134    ) -> Result<Rc<dyn Exporter>, ExportError>;
135
136    /// Find an exporter by filename, or by id.
137    fn find_exporter(
138        &self,
139        filename: &std::path::Path,
140        id: &Option<Id>,
141    ) -> Result<Rc<dyn Exporter>, ExportError> {
142        match id {
143            Some(id) => self.exporter_by_id(id),
144            None => self.exporter_by_filename(filename),
145        }
146    }
147}
148
149impl ExporterAccess for ExporterRegistry {
150    fn exporter_by_id(&self, id: &Id) -> Result<Rc<dyn Exporter>, ExportError> {
151        match self.io.by_id(id) {
152            Some(exporter) => Ok(exporter),
153            None => Err(ExportError::NoExporterWithId(id.clone())),
154        }
155    }
156
157    fn exporter_by_filename(
158        &self,
159        filename: &std::path::Path,
160    ) -> Result<Rc<dyn Exporter>, ExportError> {
161        self.by_filename(filename)
162    }
163}