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