Skip to main content

microcad_lang/render/
context.rs

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