microcad_lang/render/
context.rs

1// Copyright © 2025 The µcad authors <info@ucad.xyz>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4//! Render context
5
6use std::sync::mpsc;
7
8use microcad_core::RenderResolution;
9
10use crate::{model::Model, rc::RcMut, render::*};
11
12/// Our progress sender.
13pub type ProgressTx = mpsc::Sender<f32>;
14
15/// The render context.
16///
17/// Keeps a stack of model nodes and the render cache.
18#[derive(Default)]
19pub struct RenderContext {
20    /// Model stack.
21    pub model_stack: Vec<Model>,
22
23    /// Optional render cache.
24    pub cache: Option<RcMut<RenderCache>>,
25
26    /// The number of models to be rendered.
27    models_to_render: usize,
28
29    /// The number of model that been been rendered.
30    models_rendered: usize,
31
32    /// Progress is given as a percentage between 0.0 and 100.0.
33    pub progress_tx: Option<ProgressTx>,
34}
35
36impl RenderContext {
37    /// Initialize context with current model and prerender model.
38    pub fn new(
39        model: &Model,
40        resolution: RenderResolution,
41        cache: Option<RcMut<RenderCache>>,
42        progress_tx: Option<ProgressTx>,
43    ) -> RenderResult<Self> {
44        Ok(Self {
45            model_stack: vec![model.clone()],
46            cache,
47            models_to_render: model.prerender(resolution)?,
48            models_rendered: 0,
49            progress_tx,
50        })
51    }
52
53    /// The current model (panics if it is none).
54    pub fn model(&self) -> Model {
55        self.model_stack.last().expect("A model").clone()
56    }
57
58    /// Run the closure `f` within the given `model`.
59    pub fn with_model<T>(&mut self, model: Model, f: impl FnOnce(&mut RenderContext) -> T) -> T {
60        self.model_stack.push(model);
61        let result = f(self);
62        self.model_stack.pop();
63
64        self.step();
65
66        result
67    }
68
69    /// Make a single progress step. A progress signal is sent with each new percentage.
70    fn step(&mut self) {
71        let old_percent = self.progress_in_percent();
72        self.models_rendered += 1;
73        let new_percent = self.progress_in_percent();
74
75        // Check if integer percentage increased
76        if (old_percent.floor() as u32) < (new_percent.floor() as u32)
77            && let Some(progress_tx) = &mut self.progress_tx
78        {
79            progress_tx.send(new_percent).expect("No error");
80        }
81    }
82
83    /// Return render progress in percent.
84    pub fn progress_in_percent(&self) -> f32 {
85        (self.models_rendered as f32 / self.models_to_render as f32) * 100.0
86    }
87
88    /// Update a 2D geometry if it is not in cache.
89    pub fn update_2d<T: Into<WithBounds2D<Geometry2D>>>(
90        &mut self,
91        f: impl FnOnce(&mut RenderContext, Model) -> RenderResult<T>,
92    ) -> RenderResult<Geometry2DOutput> {
93        let model = self.model();
94        let hash = model.computed_hash();
95
96        match self.cache.clone() {
97            Some(cache) => {
98                {
99                    let mut cache = cache.borrow_mut();
100                    if let Some(GeometryOutput::Geometry2D(geo)) = cache.get(&hash) {
101                        return Ok(geo.clone());
102                    }
103                }
104                {
105                    let (geo, cost) = self.call_with_cost(model, f)?;
106                    let geo: Geometry2DOutput = Rc::new(geo.into());
107                    let mut cache = cache.borrow_mut();
108                    cache.insert_with_cost(hash, geo.clone(), cost);
109                    Ok(geo)
110                }
111            }
112            None => Ok(Rc::new(f(self, model)?.into())),
113        }
114    }
115
116    /// Update a 3D geometry if it is not in cache.
117    pub fn update_3d<T: Into<WithBounds3D<Geometry3D>>>(
118        &mut self,
119        f: impl FnOnce(&mut RenderContext, Model) -> RenderResult<T>,
120    ) -> RenderResult<Geometry3DOutput> {
121        let model = self.model();
122        let hash = model.computed_hash();
123        match self.cache.clone() {
124            Some(cache) => {
125                {
126                    let mut cache = cache.borrow_mut();
127                    if let Some(GeometryOutput::Geometry3D(geo)) = cache.get(&hash) {
128                        return Ok(geo.clone());
129                    }
130                }
131                {
132                    let (geo, cost) = self.call_with_cost(model, f)?;
133                    let geo: Geometry3DOutput = Rc::new(geo.into());
134                    let mut cache = cache.borrow_mut();
135                    cache.insert_with_cost(hash, geo.clone(), cost);
136                    Ok(geo)
137                }
138            }
139            None => Ok(Rc::new(f(self, model)?.into())),
140        }
141    }
142
143    /// Return current render resolution.
144    pub fn current_resolution(&self) -> RenderResolution {
145        self.model().borrow().resolution()
146    }
147
148    // Return the generated item and the number of milliseconds.
149    fn call_with_cost<T>(
150        &mut self,
151        model: Model,
152        f: impl FnOnce(&mut RenderContext, Model) -> RenderResult<T>,
153    ) -> RenderResult<(T, f64)> {
154        use std::time::Instant;
155        let start = Instant::now();
156
157        let r = f(self, model)?;
158
159        let duration = start.elapsed();
160        Ok((r, (duration.as_nanos() as f64) / 1_000_000.0))
161    }
162}