microcad_lang/render/
mod.rs

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