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