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<()> {
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(())
157    }
158}
159
160impl CalcBounds2D for Model {
161    fn calc_bounds_2d(&self) -> Bounds2D {
162        let self_ = self.borrow();
163        match &self_.output().geometry {
164            Some(GeometryOutput::Geometry2D(geometry)) => geometry.bounds.clone(),
165            Some(GeometryOutput::Geometry3D(_)) => Bounds2D::default(),
166            None => Bounds2D::default(),
167        }
168    }
169}
170
171/// This implementation renders a [`Geometry2D`] out of a [`Model`].
172///
173/// Notes:
174/// * The impl attaches the output geometry to the model's render output.
175/// * It is assumed the model has been pre-rendered.
176impl RenderWithContext<Geometry2DOutput> for Model {
177    fn render_with_context(&self, context: &mut RenderContext) -> RenderResult<Geometry2DOutput> {
178        context.with_model(self.clone(), |context| {
179            let model = context.model();
180            let geometry: Geometry2DOutput = {
181                let model_ = model.borrow();
182                let output = model.render_output_type();
183                match output {
184                    OutputType::Geometry2D => {
185                        match model_.element() {
186                            // A group geometry will render the child geometry
187                            Element::BuiltinWorkpiece(builtin_workpiece) => {
188                                Ok(builtin_workpiece.render_with_context(context)?)
189                            }
190                            _ => Ok(model_.children.render_with_context(context)?),
191                        }
192                    }
193                    output_type => Err(RenderError::InvalidOutputType(output_type)),
194                }
195            }?;
196
197            self.borrow_mut()
198                .output_mut()
199                .set_geometry(GeometryOutput::Geometry2D(geometry.clone()));
200            Ok(geometry)
201        })
202    }
203}
204
205/// This implementation renders a [`Geometry3D`] out of a [`Model`].
206///
207/// Notes:
208/// * The impl attaches the output geometry to the model's render output.
209/// * It is assumed the model has been pre-rendered.
210impl RenderWithContext<Geometry3DOutput> for Model {
211    fn render_with_context(&self, context: &mut RenderContext) -> RenderResult<Geometry3DOutput> {
212        context.with_model(self.clone(), |context| {
213            let model = context.model();
214            let geometry: Geometry3DOutput = {
215                let model_ = model.borrow();
216                let output = model.render_output_type();
217                match output {
218                    OutputType::Geometry3D => {
219                        match model_.element() {
220                            // A group geometry will render the child geometry
221                            Element::BuiltinWorkpiece(builtin_workpiece) => {
222                                builtin_workpiece.render_with_context(context)
223                            }
224                            _ => model_.children.render_with_context(context),
225                        }
226                    }
227                    output_type => Err(RenderError::InvalidOutputType(output_type)),
228                }
229            }?;
230
231            self.borrow_mut()
232                .output_mut()
233                .set_geometry(GeometryOutput::Geometry3D(geometry.clone()));
234            Ok(geometry)
235        })
236    }
237}
238
239impl RenderWithContext<Model> for Model {
240    fn render_with_context(&self, context: &mut RenderContext) -> RenderResult<Model> {
241        match self.render_output_type() {
242            OutputType::Geometry2D => {
243                let _: Geometry2DOutput = self.render_with_context(context)?;
244            }
245            OutputType::Geometry3D => {
246                let _: Geometry3DOutput = self.render_with_context(context)?;
247            }
248            _ => {
249                return Err(RenderError::NothingToRender);
250            }
251        }
252        log::trace!("Finished render:\n{}", FormatTree(self));
253
254        Ok(self.clone())
255    }
256}
257
258impl RenderWithContext<Geometries2D> for Models {
259    fn render_with_context(&self, context: &mut RenderContext) -> RenderResult<Geometries2D> {
260        let mut geometries = Vec::new();
261        for model in self.iter() {
262            let geo: Geometry2DOutput = model.render_with_context(context)?;
263            geometries.push(Rc::new(geo.inner.clone()));
264        }
265        Ok(geometries.into_iter().collect())
266    }
267}
268
269impl RenderWithContext<Geometry2DOutput> for Models {
270    fn render_with_context(&self, context: &mut RenderContext) -> RenderResult<Geometry2DOutput> {
271        match self.len() {
272            0 => Err(RenderError::NothingToRender),
273            1 => self.first().expect("One item").render_with_context(context),
274            _ => Ok(Rc::new(
275                Geometry2D::Collection(self.render_with_context(context)?).into(),
276            )),
277        }
278    }
279}
280
281impl RenderWithContext<Geometries3D> for Models {
282    fn render_with_context(&self, context: &mut RenderContext) -> RenderResult<Geometries3D> {
283        let mut geometries = Vec::new();
284        for model in self.iter() {
285            let geo: Geometry3DOutput = model.render_with_context(context)?;
286            geometries.push(Rc::new(geo.inner.clone()));
287        }
288        Ok(geometries.into_iter().collect())
289    }
290}
291
292impl RenderWithContext<Geometry3DOutput> for Models {
293    fn render_with_context(&self, context: &mut RenderContext) -> RenderResult<Geometry3DOutput> {
294        match self.len() {
295            0 => Err(RenderError::NothingToRender),
296            1 => self.first().expect("One item").render_with_context(context),
297            _ => Ok(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        match self.call()? {
307            BuiltinWorkpieceOutput::Primitive2D(primitive) => {
308                primitive.render_with_context(context)
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                Ok(Rc::new(output.transformed_2d(&transform.mat2d())))
315            }
316            BuiltinWorkpieceOutput::Operation(operation) => operation.process_2d(context),
317            _ => unreachable!(),
318        }
319    }
320}
321
322impl RenderWithContext<Geometry3DOutput> for BuiltinWorkpiece {
323    fn render_with_context(&self, context: &mut RenderContext) -> RenderResult<Geometry3DOutput> {
324        match self.call()? {
325            BuiltinWorkpieceOutput::Primitive3D(primitive) => {
326                primitive.render_with_context(context)
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                Ok(Rc::new(output.transformed_3d(&transform.mat3d())))
333            }
334            BuiltinWorkpieceOutput::Operation(operation) => operation.process_3d(context),
335            _ => unreachable!(),
336        }
337    }
338}