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