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        [
65            parameter!(filename: String),
66            parameter!(resolution: Length = 0.1 /*mm*/),
67        ]
68        .into_iter()
69        .collect()
70    }
71
72    /// Export the model if the model is marked for export.
73    fn export(&self, model: &Model, filename: &std::path::Path) -> Result<Value, ExportError>;
74
75    /// The expected model output type of this exporter.
76    ///
77    /// Reimplement this function when your export output format only accepts specific model output types.
78    fn output_type(&self) -> OutputType {
79        OutputType::NotDetermined
80    }
81}
82
83/// Exporter registry.
84///
85/// A database in which all exporters are stored.
86///
87/// The registry is used to find exporters by their id and their file extension.
88#[derive(Default)]
89pub struct ExporterRegistry {
90    io: FileIoRegistry<Rc<dyn Exporter>>,
91}
92
93impl ExporterRegistry {
94    /// Create new registry.
95    pub fn new() -> Self {
96        Self {
97            io: FileIoRegistry::default(),
98        }
99    }
100
101    /// Add new exporter to the registry.
102    ///
103    /// TODO Error handling.
104    pub fn insert(mut self, exporter: impl Exporter + 'static) -> Self {
105        let rc = Rc::new(exporter);
106        self.io.insert(rc);
107        self
108    }
109
110    /// Get exporter by filename.
111    pub fn by_filename(
112        &self,
113        filename: impl AsRef<std::path::Path>,
114    ) -> Result<Rc<dyn Exporter>, ExportError> {
115        let importers = self.io.by_filename(filename.as_ref());
116        match importers.len() {
117            0 => Err(ExportError::NoExporterForFile(std::path::PathBuf::from(
118                filename.as_ref(),
119            ))),
120            1 => Ok(importers.first().expect("One importer").clone()),
121            _ => Err(ExportError::MultipleExportersForFileExtension(
122                importers.iter().map(|importer| importer.id()).collect(),
123            )),
124        }
125    }
126}
127
128/// Exporter access.
129pub trait ExporterAccess {
130    /// Get exporter by id.
131    fn exporter_by_id(&self, id: &Id) -> Result<Rc<dyn Exporter>, ExportError>;
132
133    /// Get exporter by filename.
134    fn exporter_by_filename(
135        &self,
136        filename: &std::path::Path,
137    ) -> Result<Rc<dyn Exporter>, ExportError>;
138
139    /// Find an exporter by filename, or by id.
140    fn find_exporter(
141        &self,
142        filename: &std::path::Path,
143        id: &Option<Id>,
144    ) -> Result<Rc<dyn Exporter>, ExportError> {
145        match id {
146            Some(id) => self.exporter_by_id(id),
147            None => self.exporter_by_filename(filename),
148        }
149    }
150}
151
152impl ExporterAccess for ExporterRegistry {
153    fn exporter_by_id(&self, id: &Id) -> Result<Rc<dyn Exporter>, ExportError> {
154        match self.io.by_id(id) {
155            Some(exporter) => Ok(exporter),
156            None => Err(ExportError::NoExporterWithId(id.clone())),
157        }
158    }
159
160    fn exporter_by_filename(
161        &self,
162        filename: &std::path::Path,
163    ) -> Result<Rc<dyn Exporter>, ExportError> {
164        self.by_filename(filename)
165    }
166}