Skip to main content

microcad_lang/render/
mod.rs

1// Copyright © 2025-2026 The µcad authors <info@microcad.xyz>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4//! Model methods and trait implementations for rendering.
5
6mod attribute;
7mod cache;
8mod context;
9mod output;
10
11use std::rc::Rc;
12
13pub use attribute::*;
14pub use cache::*;
15pub use context::*;
16use microcad_lang_base::FormatTree;
17pub use output::*;
18
19use cgmath::SquareMatrix;
20use microcad_core::*;
21use miette::Diagnostic;
22use thiserror::Error;
23
24use crate::{
25    builtin::{BuiltinWorkbenchKind, BuiltinWorkpiece, BuiltinWorkpieceOutput},
26    model::*,
27};
28
29/// An error that occurred during rendering.
30#[derive(Debug, Error, Diagnostic)]
31pub enum RenderError {
32    /// Invalid output type.
33    #[error("Invalid output type: {0}")]
34    InvalidOutputType(OutputType),
35
36    /// Nothing to render.
37    #[error("Nothing to render")]
38    NothingToRender,
39}
40
41/// A result from rendering a model.
42pub type RenderResult<T> = Result<T, RenderError>;
43
44/// The render trait.
45pub trait RenderWithContext<T> {
46    /// Render method.
47    fn render_with_context(&self, context: &mut RenderContext) -> RenderResult<T>;
48}
49
50impl Element {
51    /// Fetch the local matrix
52    pub fn get_affine_transform(&self) -> RenderResult<Option<AffineTransform>> {
53        match &self {
54            Element::BuiltinWorkpiece(builtin_workpiece) => match builtin_workpiece.kind {
55                BuiltinWorkbenchKind::Transform => match builtin_workpiece.call()? {
56                    BuiltinWorkpieceOutput::Transform(affine_transform) => {
57                        Ok(Some(affine_transform))
58                    }
59                    _ => unreachable!(),
60                },
61                _ => Ok(None),
62            },
63            _ => Ok(None),
64        }
65    }
66}
67
68impl ModelInner {
69    /// Get render resolution.
70    pub fn resolution(&self) -> RenderResolution {
71        let output = self.output.as_ref().expect("Some render output.");
72        output
73            .resolution
74            .as_ref()
75            .expect("Some resolution.")
76            .clone()
77    }
78}
79
80impl Model {
81    /// Pre-render the model.
82    ///
83    /// Pre-rendering create as render output and calculates the matrices, resolutions and hashes of a model.
84    pub fn prerender(&self, resolution: RenderResolution) -> RenderResult<usize> {
85        pub fn create_render_output(model: &Model) -> RenderResult<()> {
86            let output = RenderOutput::new(model)?;
87            {
88                let mut model_ = model.borrow_mut();
89                model_.output = Some(output);
90            };
91
92            model
93                .borrow()
94                .children
95                .iter()
96                .try_for_each(create_render_output)
97        }
98
99        pub fn set_world_matrix(model: &Model, matrix: Mat4) -> RenderResult<()> {
100            let world_matrix = {
101                let mut model_ = model.borrow_mut();
102                let output = model_.output.as_mut().expect("Output");
103                let world_matrix = matrix * output.local_matrix().unwrap_or(Mat4::identity());
104                output.set_world_matrix(world_matrix);
105                world_matrix
106            };
107
108            model
109                .borrow()
110                .children
111                .iter()
112                .try_for_each(|model| set_world_matrix(model, world_matrix))
113        }
114
115        /// Set the resolution for this model.
116        pub fn set_resolution(model: &Model, resolution: RenderResolution) {
117            let resolution = match model.borrow().attributes().get_resolution() {
118                Some(resolution_attribute) => RenderResolution {
119                    linear: match resolution_attribute {
120                        ResolutionAttribute::Absolute(linear) => linear,
121                        ResolutionAttribute::Relative(factor) =>
122                        // Example: A relative resolution of 200% scales an absolution resolution from 0.1mm to 0.5mm.
123                        {
124                            resolution.linear / factor
125                        }
126                    },
127                },
128                None => resolution,
129            };
130
131            let new_resolution = {
132                let mut model_ = model.borrow_mut();
133                let output = model_.output.as_mut().expect("Output");
134                let resolution = resolution * output.local_matrix().unwrap_or(Mat4::identity());
135                output.set_resolution(resolution.clone());
136                resolution
137            };
138
139            model.borrow().children.iter().for_each(|model| {
140                set_resolution(model, new_resolution.clone());
141            });
142        }
143
144        // Create specific render output with local matrix.
145        create_render_output(self)?;
146
147        // Calculate the world matrix.
148        set_world_matrix(self, Mat4::identity())?;
149
150        // Calculate the resolution for the model.
151        set_resolution(self, resolution);
152
153        log::trace!("Finished prerender:\n{}", FormatTree(self));
154
155        Ok(self
156            .descendants()
157            .filter(|model| !model.has_no_output())
158            .count())
159    }
160}
161
162impl CalcBounds2D for Model {
163    fn calc_bounds_2d(&self) -> Bounds2D {
164        let self_ = self.borrow();
165        match &self_.output().geometry {
166            Some(GeometryOutput::Geometry2D(geometry)) => geometry.bounds.clone(),
167            Some(GeometryOutput::Geometry3D(_)) => Bounds2D::default(),
168            None => Bounds2D::default(),
169        }
170    }
171}
172
173/// This implementation renders a [`Geometry2D`] out of a [`Model`].
174///
175/// Notes:
176/// * The impl attaches the output geometry to the model's render output.
177/// * It is assumed the model has been pre-rendered.
178impl RenderWithContext<Geometry2DOutput> for Model {
179    fn render_with_context(&self, context: &mut RenderContext) -> RenderResult<Geometry2DOutput> {
180        context.with_model(self.clone(), |context| {
181            let model = context.model();
182            let geometry: Geometry2DOutput = {
183                let model_ = model.borrow();
184                let output = model.render_output_type();
185                match output {
186                    OutputType::Geometry2D => {
187                        match model_.element() {
188                            // A group geometry will render the child geometry
189                            Element::BuiltinWorkpiece(builtin_workpiece) => {
190                                Ok(builtin_workpiece.render_with_context(context)?)
191                            }
192                            _ => Ok(model_.children.render_with_context(context)?),
193                        }
194                    }
195                    output_type => Err(RenderError::InvalidOutputType(output_type)),
196                }
197            }?;
198
199            self.borrow_mut()
200                .output_mut()
201                .set_geometry(GeometryOutput::Geometry2D(geometry.clone()));
202            Ok(geometry)
203        })
204    }
205}
206
207/// This implementation renders a [`Geometry3D`] out of a [`Model`].
208///
209/// Notes:
210/// * The impl attaches the output geometry to the model's render output.
211/// * It is assumed the model has been pre-rendered.
212impl RenderWithContext<Geometry3DOutput> for Model {
213    fn render_with_context(&self, context: &mut RenderContext) -> RenderResult<Geometry3DOutput> {
214        context.with_model(self.clone(), |context| {
215            let model = context.model();
216            let geometry: Geometry3DOutput = {
217                let model_ = model.borrow();
218                let output = model.render_output_type();
219                match output {
220                    OutputType::Geometry3D => {
221                        match model_.element() {
222                            // A group geometry will render the child geometry
223                            Element::BuiltinWorkpiece(builtin_workpiece) => {
224                                builtin_workpiece.render_with_context(context)
225                            }
226                            _ => model_.children.render_with_context(context),
227                        }
228                    }
229                    output_type => Err(RenderError::InvalidOutputType(output_type)),
230                }
231            }?;
232
233            self.borrow_mut()
234                .output_mut()
235                .set_geometry(GeometryOutput::Geometry3D(geometry.clone()));
236            Ok(geometry)
237        })
238    }
239}
240
241impl RenderWithContext<Model> for Model {
242    fn render_with_context(&self, context: &mut RenderContext) -> RenderResult<Model> {
243        match self.render_output_type() {
244            OutputType::Geometry2D => {
245                let _: Geometry2DOutput = self.render_with_context(context)?;
246            }
247            OutputType::Geometry3D => {
248                let _: Geometry3DOutput = self.render_with_context(context)?;
249            }
250            _ => {
251                return Err(RenderError::NothingToRender);
252            }
253        }
254        log::trace!("Finished render:\n{}", FormatTree(self));
255
256        Ok(self.clone())
257    }
258}
259
260impl RenderWithContext<Geometries2D> for Models {
261    fn render_with_context(&self, context: &mut RenderContext) -> RenderResult<Geometries2D> {
262        let mut geometries = Vec::new();
263        for model in self.iter() {
264            let geo: Geometry2DOutput = model.render_with_context(context)?;
265            geometries.push(Rc::new(geo.inner.clone()));
266        }
267        Ok(geometries.into_iter().collect())
268    }
269}
270
271impl RenderWithContext<Geometry2DOutput> for Models {
272    fn render_with_context(&self, context: &mut RenderContext) -> RenderResult<Geometry2DOutput> {
273        match self.len() {
274            0 => Err(RenderError::NothingToRender),
275            1 => self.first().expect("One item").render_with_context(context),
276            _ => Ok(Rc::new(
277                Geometry2D::Collection(self.render_with_context(context)?).into(),
278            )),
279        }
280    }
281}
282
283impl RenderWithContext<Geometries3D> for Models {
284    fn render_with_context(&self, context: &mut RenderContext) -> RenderResult<Geometries3D> {
285        let mut geometries = Vec::new();
286        for model in self.iter() {
287            let geo: Geometry3DOutput = model.render_with_context(context)?;
288            geometries.push(Rc::new(geo.inner.clone()));
289        }
290        Ok(geometries.into_iter().collect())
291    }
292}
293
294impl RenderWithContext<Geometry3DOutput> for Models {
295    fn render_with_context(&self, context: &mut RenderContext) -> RenderResult<Geometry3DOutput> {
296        match self.len() {
297            0 => Err(RenderError::NothingToRender),
298            1 => self.first().expect("One item").render_with_context(context),
299            _ => Ok(Rc::new(
300                Geometry3D::Collection(self.render_with_context(context)?).into(),
301            )),
302        }
303    }
304}
305
306impl RenderWithContext<Geometry2DOutput> for BuiltinWorkpiece {
307    fn render_with_context(&self, context: &mut RenderContext) -> RenderResult<Geometry2DOutput> {
308        match self.call()? {
309            BuiltinWorkpieceOutput::Primitive2D(primitive) => {
310                primitive.render_with_context(context)
311            }
312            BuiltinWorkpieceOutput::Transform(transform) => {
313                let model = context.model();
314                let model_ = model.borrow();
315                let output: Geometry2DOutput = model_.children.render_with_context(context)?;
316                Ok(Rc::new(output.transformed_2d(&transform.mat2d())))
317            }
318            BuiltinWorkpieceOutput::Operation(operation) => operation.process_2d(context),
319            _ => unreachable!(),
320        }
321    }
322}
323
324impl RenderWithContext<Geometry3DOutput> for BuiltinWorkpiece {
325    fn render_with_context(&self, context: &mut RenderContext) -> RenderResult<Geometry3DOutput> {
326        match self.call()? {
327            BuiltinWorkpieceOutput::Primitive3D(primitive) => {
328                primitive.render_with_context(context)
329            }
330            BuiltinWorkpieceOutput::Transform(transform) => {
331                let model = context.model();
332                let model_ = model.borrow();
333                let output: Geometry3DOutput = model_.children.render_with_context(context)?;
334                Ok(Rc::new(output.transformed_3d(&transform.mat3d())))
335            }
336            BuiltinWorkpieceOutput::Operation(operation) => operation.process_3d(context),
337            _ => unreachable!(),
338        }
339    }
340}