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//! Render output type.
5
6use std::{
7    hash::{Hash, Hasher},
8    rc::Rc,
9};
10
11use microcad_core::*;
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, derive_more::From)]
23pub enum GeometryOutput {
24    /// 2D output.
25    Geometry2D(Geometry2DOutput),
26    /// 3D output.
27    Geometry3D(Geometry3DOutput),
28}
29
30impl GeometryOutput {
31    /// The radius of a centered circle that wraps the output geometries bounds on the ground.
32    pub fn ground_radius(&self) -> Length {
33        let mut bounds = match &self {
34            GeometryOutput::Geometry2D(geo2d) => geo2d.bounds.clone(),
35            GeometryOutput::Geometry3D(geo3d) => {
36                Bounds2D::new(geo3d.bounds.min.truncate(), geo3d.bounds.max.truncate())
37            }
38        };
39        bounds.extend_by_point(Vec2::new(0.0, 0.0));
40        Length::mm(bounds.radius())
41    }
42
43    /// The radius of a centered sphere, that wrap the geometries bounds.
44    pub fn scene_radius(&self) -> Length {
45        let mut bounds = match &self {
46            GeometryOutput::Geometry2D(geo2d) => {
47                Bounds3D::new(geo2d.bounds.min.extend(0.0), geo2d.bounds.max.extend(0.0))
48            }
49            GeometryOutput::Geometry3D(geo3d) => geo3d.bounds.clone(),
50        };
51        bounds.extend_by_point(Vec3::new(0.0, 0.0, 0.0));
52        Length::mm(bounds.radius())
53    }
54}
55
56impl From<Geometry2D> for GeometryOutput {
57    fn from(geo: Geometry2D) -> Self {
58        Self::Geometry2D(Rc::new(geo.into()))
59    }
60}
61
62impl From<Geometry3D> for GeometryOutput {
63    fn from(geo: Geometry3D) -> Self {
64        Self::Geometry3D(Rc::new(geo.into()))
65    }
66}
67
68/// The model output when a model has been processed.
69#[derive(Debug, Clone)]
70pub struct RenderOutput {
71    /// The output (2D/3D) this render output is expected to produce.
72    pub output_type: OutputType,
73    /// Local transformation matrix.
74    pub local_matrix: Option<Mat4>,
75    /// World transformation matrix.
76    pub world_matrix: Option<Mat4>,
77    /// The render resolution, calculated from transformation matrix.
78    pub resolution: Option<RenderResolution>,
79    /// The output geometry.
80    pub geometry: Option<GeometryOutput>,
81    /// Render attributes.
82    pub attributes: RenderAttributes,
83    /// Computed model hash.
84    hash: HashId,
85}
86
87impl RenderOutput {
88    /// Create new render output for model.
89    pub fn new(model: &Model) -> RenderResult<Self> {
90        let output_type = model.deduce_output_type();
91        let mut hasher = rustc_hash::FxHasher::default();
92        model.hash(&mut hasher);
93        let hash = hasher.finish();
94        let local_matrix = model
95            .borrow()
96            .element
97            .get_affine_transform()?
98            .map(|affine_transform| affine_transform.mat3d());
99
100        Ok(RenderOutput {
101            output_type,
102            local_matrix,
103            world_matrix: None,
104            resolution: None,
105            geometry: None,
106            attributes: model.into(),
107            hash,
108        })
109    }
110
111    /// Set the world matrix for render output.
112    pub fn set_world_matrix(&mut self, m: Mat4) {
113        self.world_matrix = Some(m);
114    }
115
116    /// Set the 2D geometry as render output.
117    pub fn set_geometry(&mut self, geo: GeometryOutput) {
118        self.geometry = Some(geo)
119    }
120
121    /// Get render resolution.
122    pub fn resolution(&self) -> &Option<RenderResolution> {
123        &self.resolution
124    }
125
126    /// Set render resolution.
127    pub fn set_resolution(&mut self, render_resolution: RenderResolution) {
128        self.resolution = Some(render_resolution);
129    }
130
131    /// Local matrix.
132    pub fn local_matrix(&self) -> Option<Mat4> {
133        self.local_matrix
134    }
135
136    /// The radius of a centered circle that wraps the output geometries bounds on the ground.
137    pub fn ground_radius(&self) -> Length {
138        self.geometry
139            .as_ref()
140            .map(|geo| geo.ground_radius())
141            .unwrap_or_default()
142    }
143
144    /// The radius of a centered sphere that wraps the output geometries bounds.
145    pub fn scene_radius(&self) -> Length {
146        self.geometry
147            .as_ref()
148            .map(|geo| geo.scene_radius())
149            .unwrap_or_default()
150    }
151}
152
153impl std::fmt::Display for RenderOutput {
154    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
155        write!(
156            f,
157            "{output_type} ({hash:X}): {geo} {resolution}",
158            output_type = match self.output_type {
159                OutputType::Geometry2D => "2D",
160                OutputType::Geometry3D => "3D",
161                OutputType::InvalidMixed => "Mixed",
162                OutputType::NotDetermined => "?",
163            },
164            hash = self.computed_hash(),
165            geo = match &self.geometry {
166                Some(GeometryOutput::Geometry2D(geo)) => geo.name(),
167                Some(GeometryOutput::Geometry3D(geo)) => geo.name(),
168                None => "",
169            },
170            resolution = match &self.resolution {
171                Some(resolution) => resolution.to_string(),
172                None => "".to_string(),
173            },
174        )?;
175        Ok(())
176    }
177}
178
179impl ComputedHash for RenderOutput {
180    fn computed_hash(&self) -> HashId {
181        self.hash
182    }
183}
184
185impl CalcBounds2D for RenderOutput {
186    fn calc_bounds_2d(&self) -> Bounds2D {
187        match &self.geometry {
188            Some(GeometryOutput::Geometry2D(output)) => output.bounds.clone(),
189            _ => Bounds2D::default(),
190        }
191    }
192}
193
194impl CalcBounds3D for RenderOutput {
195    fn calc_bounds_3d(&self) -> Bounds3D {
196        match &self.geometry {
197            Some(GeometryOutput::Geometry3D(output)) => output.bounds.clone(),
198            _ => Bounds3D::default(),
199        }
200    }
201}