microcad_lang/render/
output.rs

1// Copyright © 2025 The µcad authors <info@ucad.xyz>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4//! Model output types.
5
6use std::{
7    hash::{Hash, Hasher},
8    rc::Rc,
9};
10
11use microcad_core::{Geometry2D, Geometry3D, Mat3, Mat4, RenderResolution};
12
13use crate::{model::*, render::*};
14
15/// Geometry 2D type alias.
16pub type Geometry2DOutput = Rc<WithBounds2D<Geometry2D>>;
17
18/// Geometry 3D type alias.
19pub type Geometry3DOutput = Rc<WithBounds3D<Geometry3D>>;
20
21/// Geometry output to be stored in the render cache.
22#[derive(Debug, Clone)]
23pub enum GeometryOutput {
24    /// 2D output.
25    Geometry2D(Geometry2DOutput),
26    /// 3D output.
27    Geometry3D(Geometry3DOutput),
28}
29
30impl From<Geometry2D> for GeometryOutput {
31    fn from(geo: Geometry2D) -> Self {
32        Self::Geometry2D(Rc::new(geo.into()))
33    }
34}
35
36impl From<Geometry3D> for GeometryOutput {
37    fn from(geo: Geometry3D) -> Self {
38        Self::Geometry3D(Rc::new(geo.into()))
39    }
40}
41
42impl From<Geometry2DOutput> for GeometryOutput {
43    fn from(geo: Geometry2DOutput) -> Self {
44        Self::Geometry2D(geo)
45    }
46}
47
48impl From<Geometry3DOutput> for GeometryOutput {
49    fn from(geo: Geometry3DOutput) -> Self {
50        Self::Geometry3D(geo)
51    }
52}
53
54/// The model output when a model has been processed.
55#[derive(Debug, Clone)]
56pub enum RenderOutput {
57    /// 2D render output.
58    Geometry2D {
59        /// Local transformation matrix.
60        local_matrix: Option<Mat3>,
61        /// World transformation matrix.
62        world_matrix: Option<Mat3>,
63        /// The render resolution, calculated from transformation matrix.
64        resolution: Option<RenderResolution>,
65        /// The output geometry.
66        geometry: Option<Geometry2DOutput>,
67        /// Computed model hash.
68        hash: HashId,
69    },
70
71    /// 3D render output.
72    Geometry3D {
73        /// Local transformation matrix.
74        local_matrix: Option<Mat4>,
75        /// World transformation matrix.
76        world_matrix: Option<Mat4>,
77        /// The render resolution, calculated from transformation matrix.
78        resolution: Option<RenderResolution>,
79        /// The output geometry.
80        geometry: Option<Geometry3DOutput>,
81        /// Computed model hash.
82        hash: HashId,
83    },
84}
85
86impl RenderOutput {
87    /// Create new render output for model.
88    pub fn new(model: &Model) -> RenderResult<Self> {
89        let output_type = model.deduce_output_type();
90        let mut hasher = rustc_hash::FxHasher::default();
91        model.hash(&mut hasher);
92        let hash = hasher.finish();
93
94        match output_type {
95            OutputType::Geometry2D => {
96                let local_matrix = model
97                    .borrow()
98                    .element
99                    .get_affine_transform()?
100                    .map(|affine_transform| affine_transform.mat2d());
101
102                Ok(RenderOutput::Geometry2D {
103                    local_matrix,
104                    world_matrix: None,
105                    resolution: None,
106                    geometry: None,
107                    hash,
108                })
109            }
110
111            OutputType::Geometry3D => {
112                let local_matrix = model
113                    .borrow()
114                    .element
115                    .get_affine_transform()?
116                    .map(|affine_transform| affine_transform.mat3d());
117
118                Ok(RenderOutput::Geometry3D {
119                    local_matrix,
120                    world_matrix: None,
121                    resolution: None,
122                    geometry: None,
123                    hash,
124                })
125            }
126            output_type => Err(RenderError::InvalidOutputType(output_type)),
127        }
128    }
129
130    /// Set the world matrix for render output.
131    pub fn set_world_matrix(&mut self, m: Mat4) {
132        match self {
133            RenderOutput::Geometry2D { world_matrix, .. } => *world_matrix = Some(mat4_to_mat3(&m)),
134            RenderOutput::Geometry3D { world_matrix, .. } => {
135                *world_matrix = Some(m);
136            }
137        }
138    }
139
140    /// Set the 2D geometry as render output.
141    pub fn set_geometry_2d(&mut self, geo: Geometry2DOutput) {
142        match self {
143            RenderOutput::Geometry2D { geometry, .. } => *geometry = Some(geo),
144            RenderOutput::Geometry3D { .. } => unreachable!(),
145        }
146    }
147
148    /// Set the 2D geometry as render output.
149    pub fn set_geometry_3d(&mut self, geo: Geometry3DOutput) {
150        match self {
151            RenderOutput::Geometry2D { .. } => unreachable!(),
152            RenderOutput::Geometry3D { geometry, .. } => *geometry = Some(geo),
153        }
154    }
155
156    /// Get render resolution.
157    pub fn resolution(&self) -> &Option<RenderResolution> {
158        match self {
159            RenderOutput::Geometry2D { resolution, .. }
160            | RenderOutput::Geometry3D { resolution, .. } => resolution,
161        }
162    }
163
164    /// Set render resolution.
165    pub fn set_resolution(&mut self, render_resolution: RenderResolution) {
166        match self {
167            RenderOutput::Geometry2D { resolution, .. }
168            | RenderOutput::Geometry3D { resolution, .. } => *resolution = Some(render_resolution),
169        }
170    }
171
172    /// Local matrix.
173    pub fn local_matrix(&self) -> Option<Mat4> {
174        match self {
175            RenderOutput::Geometry2D { local_matrix, .. } => {
176                local_matrix.as_ref().map(mat3_to_mat4)
177            }
178            RenderOutput::Geometry3D { local_matrix, .. } => *local_matrix,
179        }
180    }
181
182    /// Get world matrix.
183    pub fn world_matrix(&self) -> Mat4 {
184        match self {
185            RenderOutput::Geometry2D { world_matrix, .. } => {
186                mat3_to_mat4(&world_matrix.expect("World matrix"))
187            }
188            RenderOutput::Geometry3D { world_matrix, .. } => world_matrix.expect("World matrix"),
189        }
190    }
191}
192
193fn mat4_to_mat3(m: &Mat4) -> Mat3 {
194    Mat3::from_cols(m.x.truncate_n(2), m.y.truncate_n(2), m.w.truncate_n(2))
195}
196
197fn mat3_to_mat4(m: &Mat3) -> Mat4 {
198    Mat4::new(
199        m.x.x, m.x.y, 0.0, m.x.z, // First column: X basis + X translation
200        m.y.x, m.y.y, 0.0, m.y.z, // Second column: Y basis + Y translation
201        0.0, 0.0, 1.0, 0.0, // Z axis: identity (no change)
202        0.0, 0.0, 0.0, 1.0, // Homogeneous row
203    )
204}
205
206impl std::fmt::Display for RenderOutput {
207    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
208        match &self {
209            RenderOutput::Geometry2D {
210                local_matrix,
211                geometry,
212                hash,
213                ..
214            } => {
215                write!(f, "2D ({hash:X}): ")?;
216                if local_matrix.is_none() && geometry.is_none() {
217                    write!(f, "(nothing to render)")?;
218                }
219                if local_matrix.is_some() {
220                    write!(f, "transform ")?;
221                }
222                if let Some(geometry) = geometry {
223                    write!(
224                        f,
225                        "{} {}",
226                        match &geometry.inner {
227                            Geometry2D::Collection(geometries) =>
228                                format!("Collection({} items)", geometries.len()),
229                            geometry => geometry.name().to_string(),
230                        },
231                        geometry.bounds
232                    )?;
233                }
234            }
235            RenderOutput::Geometry3D {
236                local_matrix,
237                geometry,
238                hash,
239                ..
240            } => {
241                write!(f, "3D ({hash:X}): ")?;
242                match (geometry, local_matrix) {
243                    (None, None) => write!(f, "(nothing to render)"),
244                    (None, Some(_)) => {
245                        write!(f, "transform")
246                    }
247                    (Some(geometry), None) => write!(f, "{}", geometry.inner.name()),
248                    (Some(geometry), Some(_)) => write!(f, "transformed {}", geometry.inner.name()),
249                }?;
250            }
251        }
252
253        if let Some(resolution) = self.resolution() {
254            write!(f, " {resolution}")?
255        }
256        Ok(())
257    }
258}
259
260impl ComputedHash for RenderOutput {
261    fn computed_hash(&self) -> HashId {
262        match self {
263            RenderOutput::Geometry2D { hash, .. } | RenderOutput::Geometry3D { hash, .. } => *hash,
264        }
265    }
266}