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