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 microcad_core::RenderResolution;
7
8use crate::{model::Model, rc::RcMut, render::*};
9
10/// The render context.
11///
12/// Keeps a stack of model nodes and the render cache.
13#[derive(Default)]
14pub struct RenderContext {
15    /// Model stack.
16    pub model_stack: Vec<Model>,
17
18    /// Optional render cache.
19    pub cache: Option<RcMut<RenderCache>>,
20}
21
22impl RenderContext {
23    /// Create default context.
24    pub fn new() -> Self {
25        Self::default()
26    }
27
28    /// Initialize context with current model and prerender model.
29    pub fn init(
30        model: &Model,
31        resolution: RenderResolution,
32        cache: Option<RcMut<RenderCache>>,
33    ) -> RenderResult<Self> {
34        model.prerender(resolution)?;
35        Ok(Self {
36            model_stack: vec![model.clone()],
37            cache,
38        })
39    }
40
41    /// The current model (panics if it is none).
42    pub fn model(&self) -> Model {
43        self.model_stack.last().expect("A model").clone()
44    }
45
46    /// Run the closure `f` within the given `model`.
47    pub fn with_model<T>(&mut self, model: Model, f: impl FnOnce(&mut RenderContext) -> T) -> T {
48        self.model_stack.push(model);
49        let result = f(self);
50        self.model_stack.pop();
51        result
52    }
53
54    /// Update a 2D geometry if it is not in cache.
55    pub fn update_2d<T: Into<WithBounds2D<Geometry2D>>>(
56        &mut self,
57        f: impl FnOnce(&mut RenderContext, Model) -> RenderResult<T>,
58    ) -> RenderResult<Geometry2DOutput> {
59        let model = self.model();
60        let hash = model.computed_hash();
61
62        match self.cache.clone() {
63            Some(cache) => {
64                {
65                    let mut cache = cache.borrow_mut();
66                    if let Some(GeometryOutput::Geometry2D(geo)) = cache.get(&hash) {
67                        return Ok(geo.clone());
68                    }
69                }
70                {
71                    let (geo, cost) = self.call_with_cost(model, f)?;
72                    let geo: Geometry2DOutput = Rc::new(geo.into());
73                    let mut cache = cache.borrow_mut();
74                    cache.insert_with_cost(hash, geo.clone(), cost);
75                    Ok(geo)
76                }
77            }
78            None => Ok(Rc::new(f(self, model)?.into())),
79        }
80    }
81
82    /// Update a 3D geometry if it is not in cache.
83    pub fn update_3d<T: Into<WithBounds3D<Geometry3D>>>(
84        &mut self,
85        f: impl FnOnce(&mut RenderContext, Model) -> RenderResult<T>,
86    ) -> RenderResult<Geometry3DOutput> {
87        let model = self.model();
88        let hash = model.computed_hash();
89        match self.cache.clone() {
90            Some(cache) => {
91                {
92                    let mut cache = cache.borrow_mut();
93                    if let Some(GeometryOutput::Geometry3D(geo)) = cache.get(&hash) {
94                        return Ok(geo.clone());
95                    }
96                }
97                {
98                    let (geo, cost) = self.call_with_cost(model, f)?;
99                    let geo: Geometry3DOutput = Rc::new(geo.into());
100                    let mut cache = cache.borrow_mut();
101                    cache.insert_with_cost(hash, geo.clone(), cost);
102                    Ok(geo)
103                }
104            }
105            None => Ok(Rc::new(f(self, model)?.into())),
106        }
107    }
108
109    /// Return current render resolution.
110    pub fn current_resolution(&self) -> RenderResolution {
111        self.model().borrow().resolution()
112    }
113
114    // Return the generated item and the number of milliseconds.
115    fn call_with_cost<T>(
116        &mut self,
117        model: Model,
118        f: impl FnOnce(&mut RenderContext, Model) -> RenderResult<T>,
119    ) -> RenderResult<(T, f64)> {
120        use std::time::Instant;
121        let start = Instant::now();
122
123        let r = f(self, model)?;
124
125        let duration = start.elapsed();
126        Ok((r, (duration.as_nanos() as f64) / 1_000_000.0))
127    }
128}